From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 21 Aug 2025 06:57:36 +0000 Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(engineering)/document-list-only/layout.tsx | 17 ++ .../(engineering)/document-list-only/page.tsx | 98 ++++++++++ .../(engineering)/document-list-ship/page.tsx | 141 ++++++++++++++ .../[formId]/[projectId]/[contractId]/page.tsx | 79 ++++++++ .../(engineering)/vendor-data/layout.tsx | 67 +++++++ .../engineering/(engineering)/vendor-data/page.tsx | 28 +++ .../(engineering)/vendor-data/tag/[id]/page.tsx | 43 +++++ .../basic-contract-template/gtc/[id]/page.tsx | 142 -------------- .../(evcp)/basic-contract-template/gtc/page.tsx | 69 ------- app/[lng]/evcp/(evcp)/document-list-ship/page.tsx | 141 ++++++++++++++ app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx | 142 ++++++++++++++ app/[lng]/evcp/(evcp)/gtc/page.tsx | 69 +++++++ app/[lng]/evcp/(evcp)/information/page.tsx | 108 +++++------ app/[lng]/evcp/(evcp)/menu-list/page.tsx | 22 ++- .../vendors/[id]/info/basic/basic-info-client.tsx | 206 ++++++++++++++++++--- .../evcp/(evcp)/vendors/[id]/info/basic/types.ts | 4 + app/api/files/[...path]/route.ts | 3 + app/api/upload/basicContract/chunk/route.ts | 156 +++++++++------- 18 files changed, 1170 insertions(+), 365 deletions(-) create mode 100644 app/[lng]/engineering/(engineering)/document-list-only/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/document-list-only/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/document-list-ship/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-data/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-data/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/document-list-ship/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/gtc/page.tsx (limited to 'app') diff --git a/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx b/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx new file mode 100644 index 00000000..17e78c0a --- /dev/null +++ b/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx @@ -0,0 +1,17 @@ +import { Shell } from "@/components/shell" +import VendorDocumentListClientEvcp from "@/components/document-lists/vendor-doc-list-client-evcp" + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default async function EvcpDocuments({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/app/[lng]/engineering/(engineering)/document-list-only/page.tsx b/app/[lng]/engineering/(engineering)/document-list-only/page.tsx new file mode 100644 index 00000000..5b49a6ef --- /dev/null +++ b/app/[lng]/engineering/(engineering)/document-list-only/page.tsx @@ -0,0 +1,98 @@ +// evcp/document-list-only/page.tsx - 전체 계약 대상 문서 목록 +import * as React from "react" +import { Suspense } from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { DocumentStagesTable } from "@/lib/vendor-document-list/plant/document-stages-table" +import { documentStageSearchParamsCache } from "@/lib/vendor-document-list/plant/document-stage-validations" +import { getDocumentStagesOnly } from "@/lib/vendor-document-list/plant/document-stages-service" + +interface IndexPageProps { + searchParams: Promise +} + +// 문서 테이블 래퍼 컴포넌트 (전체 계약용) +async function DocumentTableWrapper({ + searchParams +}: { + searchParams: SearchParams +}) { + const search = documentStageSearchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 필터 타입 변환 + const convertedFilters = validFilters.map(filter => ({ + id: (filter.id || filter.rowId) as string, + value: filter.value, + operator: (filter.operator === 'iLike' ? 'ilike' : + filter.operator === 'notILike' ? 'notin' : + filter.operator === 'isEmpty' ? 'eq' : + filter.operator === 'isNotEmpty' ? 'ne' : + filter.operator === 'isBetween' ? 'eq' : + filter.operator === 'isRelativeToToday' ? 'eq' : + filter.operator || 'eq') as 'eq' | 'in' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'like' | 'ilike' | 'notin' + })) + + // evcp: 전체 계약 대상으로 문서 조회 + const documentsPromise = getDocumentStagesOnly({ + ...search, + filters: convertedFilters, + }, -1) // 세션에서 자동으로 도메인 감지 + + return ( + + ) +} + +function TableLoadingSkeleton() { + return ( +
+
+ +
+ + +
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ + + + + + +
+ ))} +
+
+
+
+ ) +} + +// 메인 페이지 컴포넌트 +export default async function DocumentStagesManagementPage({ + searchParams +}: IndexPageProps) { + const resolvedSearchParams = await searchParams + + return ( +
+ {/* 문서 테이블 */} + }> + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx new file mode 100644 index 00000000..321ce909 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx @@ -0,0 +1,141 @@ +// page.tsx (간단한 Promise 생성과 로그인 처리) +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsShipDocuCache } from "@/lib/vendor-document-list/validations" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { LogIn } from "lucide-react" +import { getUserVendorDocumentStats, getUserVendorDocumentStatsAll, getUserVendorDocuments, getUserVendorDocumentsAll } from "@/lib/vendor-document-list/enhanced-document-service" +import { UserVendorDocumentDisplay } from "@/components/ship-vendor-document/user-vendor-document-table-container" +import { InformationButton } from "@/components/information/information-button" +import { UserVendorALLDocumentDisplay } from "@/components/ship-vendor-document-all/user-vendor-document-table-container" +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsShipDocuCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // Get session + const session = await getServerSession(authOptions) + + // Check if user is logged in + if (!session || !session.user) { + return ( + +
+
+
+

+ 문서 관리 +

+ +
+ {/*

+ 소속 회사의 모든 도서/도면을 확인하고 관리합니다. +

*/} +
+
+ +
+
+

로그인이 필요합니다

+

+ 문서를 확인하려면 먼저 로그인하세요. +

+ +
+
+
+ ) + } + + // User is logged in, get user ID + const requesterId = session.user.id ? Number(session.user.id) : null + + if (!requesterId) { + return ( + +
+
+

+ Document Management +

+
+
+
+
+

계정 오류

+

+ 사용자 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. +

+
+
+
+ ) + } + + // 검색 파라미터 정리 + const searchInput = { + ...search, + filters: validFilters, + } + + // Promise 생성 (모든 데이터를 페이지에서 처리) + const documentsPromise = getUserVendorDocumentsAll(requesterId, searchInput) + const statsPromise = getUserVendorDocumentStatsAll(requesterId) + + // Promise.all로 감싸서 전달 + const allPromises = Promise.all([documentsPromise, statsPromise]) + const statsResult = await documentsPromise + + + return ( + +
+
+

+ 조선 Document Management +

+

+ +

+
+
+ + }> + {/* DateRangePicker can go here */} + + + + } + > + + +
+ ) +} + diff --git a/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx new file mode 100644 index 00000000..f69aa525 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx @@ -0,0 +1,79 @@ +import DynamicTable from "@/components/form-data/form-data-table"; +import { findContractItemId, getFormData, getFormId } from "@/lib/forms/services"; + +interface IndexPageProps { + params: { + lng: string; + packageId: string; + formId: string; + projectId: string; + contractId: string; + + + }; + searchParams?: { + mode?: string; + }; +} + +export default async function FormPage({ params, searchParams }: IndexPageProps) { + // 1) 구조 분해 할당 + const resolvedParams = await params; + + // 2) searchParams도 await 필요 + const resolvedSearchParams = await searchParams; + + // 3) 구조 분해 할당 + const { lng, packageId, formId: formCode, projectId,contractId } = resolvedParams; + + // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) + const mode = resolvedSearchParams?.mode === "ENG" ? "ENG" : "IM"; // 기본값은 IM + + // 4) 변환 + let packageIdAsNumber = Number(packageId); + const contractIdAsNumber = Number(contractId); + + // packageId가 0이면 contractId와 formCode로 실제 contractItemId 찾기 + if (packageIdAsNumber === 0 && contractIdAsNumber > 0) { + console.log(`packageId가 0이므로 contractId ${contractIdAsNumber}와 formCode ${formCode}로 contractItemId 조회`); + + const foundContractItemId = await findContractItemId(contractIdAsNumber, formCode); + + if (foundContractItemId) { + console.log(`contractItemId ${foundContractItemId}를 찾았습니다. 이 값을 사용합니다.`); + packageIdAsNumber = foundContractItemId; + } else { + console.warn(`contractItemId를 찾을 수 없습니다. packageId는 계속 0으로 유지됩니다.`); + } + } + + // 5) DB 조회 + const { columns, data, editableFieldsMap } = await getFormData(formCode, packageIdAsNumber); + + + // 6) formId 및 report temp file 조회 + const { formId } = await getFormId(String(packageIdAsNumber), formCode); + + // 7) 예외 처리 + if (!columns) { + return ( +

해당 폼의 메타 정보를 불러올 수 없습니다. ENG 모드의 경우에는 SHI 관리자에게 폼 생성 요청을 하시기 바랍니다.

+ ); + } + + // 8) 렌더링 + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx b/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx new file mode 100644 index 00000000..7d00359c --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx @@ -0,0 +1,67 @@ +// app/vendor-data/layout.tsx +import * as React from "react" +import { cookies } from "next/headers" +import { Shell } from "@/components/shell" +import { getVendorProjectsAndContracts } from "@/lib/vendor-data/services" +import { VendorDataContainer } from "@/components/vendor-data/vendor-data-container" +import { InformationButton } from "@/components/information/information-button" +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default async function VendorDataLayout({ + children, +}: { + children: React.ReactNode +}) { + // evcp: 전체 계약 대상으로 프로젝트 데이터 가져오기 + const projects = await getVendorProjectsAndContracts() + + // 레이아웃 설정 쿠키 가져오기 + // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 + const cookieStore = await cookies() + + // 이제 cookieStore.get() 메서드 사용 가능 + const layout = cookieStore.get("react-resizable-panels:layout:mail") + const collapsed = cookieStore.get("react-resizable-panels:collapsed") + + const defaultLayout = layout ? JSON.parse(layout.value) : undefined + const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined + + return ( + +
+
+
+
+

+ 협력업체 데이터 입력 +

+ +
+ {/*

+ 각종 Data 입력할 수 있습니다 +

*/} +
+
+
+ +
+
+ {projects.length === 0 ? ( +
+ No projects found for this vendor. +
+ ) : ( + + {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} + {children} + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/page.tsx new file mode 100644 index 00000000..ddc21a2b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-data/page.tsx @@ -0,0 +1,28 @@ +// evcp/vendor-data/page.tsx - 전체 계약 대상 협력업체 데이터 +import * as React from "react" +import { Separator } from "@/components/ui/separator" + +export default async function IndexPage() { + return ( +
+
+

전체 계약 협력업체 데이터 대시보드

+

+ 모든 계약의 협력업체 데이터를 확인하고 관리할 수 있습니다. +

+
+ +
+
+

사용 방법

+

+ 1. 왼쪽 사이드바에서 계약을 선택하세요.
+ 2. 선택한 계약의 패키지 항목을 클릭하세요.
+ 3. 패키지의 태그 정보를 확인하고 관리할 수 있습니다.
+ 4. 폼 항목을 클릭하여 칼럼 정보를 확인하고 관리할 수 있습니다. +

+
+
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx new file mode 100644 index 00000000..7250732f --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx @@ -0,0 +1,43 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { TagsTable } from "@/lib/tags/table/tag-table" +import { searchParamsCache } from "@/lib/tags/validations" +import { getTags } from "@/lib/tags/service" + +interface IndexPageProps { + params: { + id: string + } + searchParams: Promise +} + +export default async function TagPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTags({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx deleted file mode 100644 index 0f783375..00000000 --- a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import * as React from "react" -import { notFound } from "next/navigation" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { InformationButton } from "@/components/information/information-button" - -import { - getGtcClauses, - getUsersForFilter, -} from "@/lib/gtc-contract/gtc-clauses/service" -import { getGtcDocumentById } from "@/lib/gtc-contract/service" -import { searchParamsCache } from "@/lib/gtc-contract/gtc-clauses/validations" -import { GtcClausesPageHeader } from "@/lib/gtc-contract/gtc-clauses/gtc-clauses-page-header" -import { GtcClausesTable } from "@/lib/gtc-contract/gtc-clauses/table/clause-table" - -interface GtcClausesPageProps { - params: Promise<{ id: string }> - searchParams: Promise -} - -export default async function GtcClausesPage(props: GtcClausesPageProps) { - const params = await props.params - const searchParams = await props.searchParams - const documentId = parseInt(params.id) - - if (isNaN(documentId)) { - notFound() - } - - // 문서 정보 조회 - const document = await getGtcDocumentById(documentId) - if (!document) { - notFound() - } - - const search = searchParamsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 병렬로 데이터 조회 - const promises = Promise.all([ - getGtcClauses({ - ...search, - filters: validFilters, - documentId, - }), - getUsersForFilter() - ]) - - return ( - - {/* 헤더 컴포넌트 */} - - - {/* 문서 정보 카드 */} -
-
-
-
최초등록일
-
{document.createdAt ? new Date(document.createdAt).toLocaleDateString('ko-KR') : '-'}
-
-
-
최초등록자
-
{document.createdByName || '-'}
-
-
-
최종수정일
-
{document.updatedAt ? new Date(document.updatedAt).toLocaleDateString('ko-KR') : '-'}
-
-
-
최종수정자
-
{document.updatedByName || '-'}
-
-
- - {document.editReason && ( -
-
최종 편집사유
-
{document.editReason}
-
- )} -
- - {/* 조항 테이블 */} - - } - > - - -
- ) -} - -// 메타데이터 생성 -export async function generateMetadata(props: GtcClausesPageProps) { - const params = await props.params - const documentId = parseInt(params.id) - - if (isNaN(documentId)) { - return { - title: "GTC 조항 관리", - } - } - - try { - const document = await getGtcDocumentById(documentId) - - if (!document) { - return { - title: "GTC 조항 관리", - } - } - - const title = `GTC 조항 관리 - ${document.type === "standard" ? "표준" : "프로젝트"} v${document.revision}` - const description = document.project - ? `${document.project.name} (${document.project.code}) 프로젝트의 GTC 조항을 관리합니다.` - : "표준 GTC 조항을 관리합니다." - - return { - title, - description, - } - } catch (error) { - return { - title: "GTC 조항 관리", - } - } -} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx deleted file mode 100644 index 33c504df..00000000 --- a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { InformationButton } from "@/components/information/information-button" -import { GtcDocumentsTable } from "@/lib/gtc-contract/status/gtc-contract-table" -import { getGtcDocuments,getProjectsForFilter,getUsersForFilter } from "@/lib/gtc-contract/service" -import { searchParamsCache } from "@/lib/gtc-contract/validations" - -interface GtcPageProps { - searchParams: Promise -} - -export default async function GtcPage(props: GtcPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getGtcDocuments({ - ...search, - filters: validFilters, - }), - getProjectsForFilter(), - getUsersForFilter() - ]) - - return ( - -
-
-
-
-

- GTC 목록관리 -

- -
-
-
-
- - }> - {/* */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx b/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx new file mode 100644 index 00000000..321ce909 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx @@ -0,0 +1,141 @@ +// page.tsx (간단한 Promise 생성과 로그인 처리) +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsShipDocuCache } from "@/lib/vendor-document-list/validations" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { LogIn } from "lucide-react" +import { getUserVendorDocumentStats, getUserVendorDocumentStatsAll, getUserVendorDocuments, getUserVendorDocumentsAll } from "@/lib/vendor-document-list/enhanced-document-service" +import { UserVendorDocumentDisplay } from "@/components/ship-vendor-document/user-vendor-document-table-container" +import { InformationButton } from "@/components/information/information-button" +import { UserVendorALLDocumentDisplay } from "@/components/ship-vendor-document-all/user-vendor-document-table-container" +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsShipDocuCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // Get session + const session = await getServerSession(authOptions) + + // Check if user is logged in + if (!session || !session.user) { + return ( + +
+
+
+

+ 문서 관리 +

+ +
+ {/*

+ 소속 회사의 모든 도서/도면을 확인하고 관리합니다. +

*/} +
+
+ +
+
+

로그인이 필요합니다

+

+ 문서를 확인하려면 먼저 로그인하세요. +

+ +
+
+
+ ) + } + + // User is logged in, get user ID + const requesterId = session.user.id ? Number(session.user.id) : null + + if (!requesterId) { + return ( + +
+
+

+ Document Management +

+
+
+
+
+

계정 오류

+

+ 사용자 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. +

+
+
+
+ ) + } + + // 검색 파라미터 정리 + const searchInput = { + ...search, + filters: validFilters, + } + + // Promise 생성 (모든 데이터를 페이지에서 처리) + const documentsPromise = getUserVendorDocumentsAll(requesterId, searchInput) + const statsPromise = getUserVendorDocumentStatsAll(requesterId) + + // Promise.all로 감싸서 전달 + const allPromises = Promise.all([documentsPromise, statsPromise]) + const statsResult = await documentsPromise + + + return ( + +
+
+

+ 조선 Document Management +

+

+ +

+
+
+ + }> + {/* DateRangePicker can go here */} + + + + } + > + + +
+ ) +} + diff --git a/app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx b/app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx new file mode 100644 index 00000000..0f783375 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx @@ -0,0 +1,142 @@ +import * as React from "react" +import { notFound } from "next/navigation" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { InformationButton } from "@/components/information/information-button" + +import { + getGtcClauses, + getUsersForFilter, +} from "@/lib/gtc-contract/gtc-clauses/service" +import { getGtcDocumentById } from "@/lib/gtc-contract/service" +import { searchParamsCache } from "@/lib/gtc-contract/gtc-clauses/validations" +import { GtcClausesPageHeader } from "@/lib/gtc-contract/gtc-clauses/gtc-clauses-page-header" +import { GtcClausesTable } from "@/lib/gtc-contract/gtc-clauses/table/clause-table" + +interface GtcClausesPageProps { + params: Promise<{ id: string }> + searchParams: Promise +} + +export default async function GtcClausesPage(props: GtcClausesPageProps) { + const params = await props.params + const searchParams = await props.searchParams + const documentId = parseInt(params.id) + + if (isNaN(documentId)) { + notFound() + } + + // 문서 정보 조회 + const document = await getGtcDocumentById(documentId) + if (!document) { + notFound() + } + + const search = searchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 병렬로 데이터 조회 + const promises = Promise.all([ + getGtcClauses({ + ...search, + filters: validFilters, + documentId, + }), + getUsersForFilter() + ]) + + return ( + + {/* 헤더 컴포넌트 */} + + + {/* 문서 정보 카드 */} +
+
+
+
최초등록일
+
{document.createdAt ? new Date(document.createdAt).toLocaleDateString('ko-KR') : '-'}
+
+
+
최초등록자
+
{document.createdByName || '-'}
+
+
+
최종수정일
+
{document.updatedAt ? new Date(document.updatedAt).toLocaleDateString('ko-KR') : '-'}
+
+
+
최종수정자
+
{document.updatedByName || '-'}
+
+
+ + {document.editReason && ( +
+
최종 편집사유
+
{document.editReason}
+
+ )} +
+ + {/* 조항 테이블 */} + + } + > + + +
+ ) +} + +// 메타데이터 생성 +export async function generateMetadata(props: GtcClausesPageProps) { + const params = await props.params + const documentId = parseInt(params.id) + + if (isNaN(documentId)) { + return { + title: "GTC 조항 관리", + } + } + + try { + const document = await getGtcDocumentById(documentId) + + if (!document) { + return { + title: "GTC 조항 관리", + } + } + + const title = `GTC 조항 관리 - ${document.type === "standard" ? "표준" : "프로젝트"} v${document.revision}` + const description = document.project + ? `${document.project.name} (${document.project.code}) 프로젝트의 GTC 조항을 관리합니다.` + : "표준 GTC 조항을 관리합니다." + + return { + title, + description, + } + } catch (error) { + return { + title: "GTC 조항 관리", + } + } +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/gtc/page.tsx b/app/[lng]/evcp/(evcp)/gtc/page.tsx new file mode 100644 index 00000000..33c504df --- /dev/null +++ b/app/[lng]/evcp/(evcp)/gtc/page.tsx @@ -0,0 +1,69 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { InformationButton } from "@/components/information/information-button" +import { GtcDocumentsTable } from "@/lib/gtc-contract/status/gtc-contract-table" +import { getGtcDocuments,getProjectsForFilter,getUsersForFilter } from "@/lib/gtc-contract/service" +import { searchParamsCache } from "@/lib/gtc-contract/validations" + +interface GtcPageProps { + searchParams: Promise +} + +export default async function GtcPage(props: GtcPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getGtcDocuments({ + ...search, + filters: validFilters, + }), + getProjectsForFilter(), + getUsersForFilter() + ]) + + return ( + +
+
+
+
+

+ GTC 목록관리 +

+ +
+
+
+
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/information/page.tsx b/app/[lng]/evcp/(evcp)/information/page.tsx index db383c32..c87471ae 100644 --- a/app/[lng]/evcp/(evcp)/information/page.tsx +++ b/app/[lng]/evcp/(evcp)/information/page.tsx @@ -1,58 +1,52 @@ -import * as React from "react" -import type { Metadata } from "next" -import { unstable_noStore as noStore } from "next/cache" - -import { Shell } from "@/components/shell" -import { getInformationLists } from "@/lib/information/service" -import { InformationClient } from "@/components/information/information-client" -import { InformationButton } from "@/components/information/information-button" - -export const metadata: Metadata = { - title: "인포메이션 관리", - description: "페이지별 도움말 및 첨부파일을 관리합니다.", -} - -interface InformationPageProps { - params: Promise<{ lng: string }> -} - -export default async function InformationPage({ params }: InformationPageProps) { - noStore() - - const { lng } = await params - - // 초기 데이터 로딩 - const initialData = await getInformationLists({ - page: 1, - perPage: 500, - search: "", - sort: [{ id: "createdAt", desc: true }], - flags: [], - filters: [], - joinOperator: "and", - pagePath: "", - pageName: "", - informationContent: "", - isActive: null, - from: "", - to: "", - }) - - return ( - -
-
-
-
-

- 인포메이션 관리 -

- -
-
-
-
- -
- ) +import * as React from "react" +import type { Metadata } from "next" +import { unstable_noStore as noStore } from "next/cache" + +import { Shell } from "@/components/shell" +import { getInformationLists } from "@/lib/information/service" +import { InformationClient } from "@/components/information/information-client" +import { InformationButton } from "@/components/information/information-button" +import { useTranslation } from "@/i18n" + +export const metadata: Metadata = { + title: "인포메이션 관리", + description: "페이지별 도움말 및 첨부파일을 관리합니다.", +} + +interface InformationPageProps { + params: Promise<{ lng: string }> +} + +export default async function InformationPage({ params }: InformationPageProps) { + noStore() + + const { lng } = await params + const { t } = await useTranslation(lng, 'menu') + + // 초기 데이터 로딩 (간단화) + const initialData = await getInformationLists() + + // 서버사이드에서 번역된 데이터 생성 + const translatedData = initialData?.data?.map(item => ({ + ...item, + translatedPageName: t(item.pageName) + })) || [] + + return ( + +
+
+
+
+

+ 인포메이션 관리 +

+ +
+
+
+
+ +
+ ) } \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/menu-list/page.tsx b/app/[lng]/evcp/(evcp)/menu-list/page.tsx index 6645127f..5a1f71a5 100644 --- a/app/[lng]/evcp/(evcp)/menu-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/menu-list/page.tsx @@ -10,13 +10,31 @@ import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; import { Shell } from "@/components/shell" import * as React from "react" import { InformationButton } from "@/components/information/information-button"; -export default async function MenuListPage() { +import { useTranslation } from "@/i18n"; +interface MenuListPageProps { + params: Promise<{ lng: string }> +} + +export default async function MenuListPage({ params }: MenuListPageProps) { + const { lng } = await params + const { t } = await useTranslation(lng, 'menu') + // 초기 데이터 로드 const [menusResult, usersResult] = await Promise.all([ getMenuAssignments(), getActiveUsers() ]); + // 서버사이드에서 번역된 메뉴 데이터 생성 + const translatedMenus = menusResult.data?.map(menu => ({ + ...menu, + sectionTitle: menu.sectionTitle || "", + translatedMenuTitle: t(menu.menuTitle || ""), + translatedSectionTitle: t(menu.sectionTitle || ""), + translatedMenuGroup: menu.menuGroup ? t(menu.menuGroup) : null, + translatedMenuDescription: menu.menuDescription ? t(menu.menuDescription) : null + })) || []; + return (
@@ -60,7 +78,7 @@ export default async function MenuListPage() { 로딩 중...
}> diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx index d00bfaa8..e92edc11 100644 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx @@ -18,6 +18,12 @@ import { toast } from "sonner"; import { VendorData, VendorFormData, VendorAttachment } from "./types"; import { updateVendorData } from "./actions"; import { noDataString } from "./constants"; +import { PQSimpleDialog } from "@/components/vendor-info/pq-simple-dialog"; +import { SiteVisitDetailDialog } from "@/lib/site-visit/site-visit-detail-dialog"; +import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog"; +import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog"; +import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service"; +import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service"; import { Table, TableBody, @@ -314,6 +320,16 @@ export default function BasicInfoClient({ }: BasicInfoClientProps) { const [editMode, setEditMode] = useState(false); const [isPending, startTransition] = useTransition(); + + // 다이얼로그 상태 + const [pqDialogOpen, setPqDialogOpen] = useState(false); + const [siteVisitDialogOpen, setSiteVisitDialogOpen] = useState(false); + const [contractDialogOpen, setContractDialogOpen] = useState(false); + const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = useState(false); + + // 각 다이얼로그에 필요한 데이터 상태 + const [selectedSiteVisitRequest, setSelectedSiteVisitRequest] = useState(null); + const [registrationData, setRegistrationData] = useState(null); const [formData, setFormData] = useState({ vendorName: initialData?.vendorName || "", representativeName: initialData?.representativeName || "", @@ -326,6 +342,8 @@ export default function BasicInfoClient({ fax: initialData?.fax || "", email: initialData?.email || "", address: initialData?.address || "", + addressDetail: initialData?.addressDetail || "", + postalCode: initialData?.postalCode || "", businessSize: initialData?.businessSize || "", country: initialData?.country || "", website: initialData?.website || "", @@ -363,6 +381,8 @@ export default function BasicInfoClient({ fax: initialData?.fax || "", email: initialData?.email || "", address: initialData?.address || "", + addressDetail: initialData?.addressDetail || "", + postalCode: initialData?.postalCode || "", businessSize: initialData?.businessSize || "", country: initialData?.country || "", website: initialData?.website || "", @@ -387,6 +407,56 @@ export default function BasicInfoClient({ ); }; + // PQ 조회 핸들러 + const handlePQView = () => { + setPqDialogOpen(true); + }; + + // 실사 정보 조회 핸들러 + const handleSiteVisitView = async () => { + try { + const siteVisitRequests = await getSiteVisitRequestsByVendorId(parseInt(vendorId)); + if (siteVisitRequests.length === 0) { + toast.info("실사 정보가 없습니다."); + return; + } + setSelectedSiteVisitRequest(siteVisitRequests[0]); // 첫 번째 실사 정보 선택 + setSiteVisitDialogOpen(true); + } catch (error) { + console.error("실사 정보 조회 오류:", error); + toast.error("실사 정보를 불러오는데 실패했습니다."); + } + }; + + // 기본계약 현황 조회 핸들러 + const handleContractView = async () => { + try { + const result = await fetchVendorRegistrationStatus(parseInt(vendorId)); + if (!result.success || !result.data) { + toast.info("기본계약 정보가 없습니다."); + return; + } + + // 등록 데이터가 있는지 확인 + const registrationRecord = result.data; + if (!registrationRecord || !registrationRecord.documentSubmissions) { + toast.info("정규등록 정보가 없습니다."); + return; + } + + setRegistrationData(registrationRecord); + setContractDialogOpen(true); + } catch (error) { + console.error("기본계약 정보 조회 오류:", error); + toast.error("기본계약 정보를 불러오는데 실패했습니다."); + } + }; + + // 추가정보 조회 핸들러 + const handleAdditionalInfoView = () => { + setAdditionalInfoDialogOpen(true); + }; + if (!initialData) { return (
@@ -459,12 +529,12 @@ export default function BasicInfoClient({ fieldKey="vendorName" onChange={(value) => updateField("vendorName", value)} /> - + /> */} updateField("phone", value)} /> - updateField("fax", value)} - /> + /> */} updateField("businessType", value)} /> - handleFileManagement("소개자료")} - /> + /> */} updateField("address", value)} /> + updateField("addressDetail", value)} + /> + updateField("postalCode", value)} + /> updateField("email", value)} /> - updateField("businessSize", value)} placeholder="기업규모를 선택하세요" - /> + /> */} - + /> */} @@ -633,7 +719,7 @@ export default function BasicInfoClient({ {/* 상세정보 */} - @@ -749,12 +835,12 @@ export default function BasicInfoClient({
} - /> + /> */} - + {/* */} {/* 매출정보 */} - } - /> + /> */} - + {/* */} {/* 실사정보 */} - } - /> + /> */} - + {/* */} {/* 계약정보 */} - @@ -1101,8 +1187,80 @@ export default function BasicInfoClient({ ))} } - /> + /> */} + + {/* */} + + {/* 추가 조회 기능 버튼들 */} +
+
상세 정보 조회
+
+ + + + + + + +
+
+ + {/* 다이얼로그들 */} + + + + + {registrationData && ( + + )} + + ); } diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts index 510ae361..ead3a44c 100644 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts @@ -118,6 +118,8 @@ export interface VendorData { vendorCode: string; taxId: string; address: string; + addressDetail: string; + postalCode: string; businessSize: string; country: string; phone: string; @@ -163,6 +165,8 @@ export interface VendorFormData { representativeBirth: string; representativePhone: string; representativeEmail: string; + addressDetail: string; + postalCode: string; phone: string; fax: string; email: string; diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts index 9ef11bc7..0bc2e22f 100644 --- a/app/api/files/[...path]/route.ts +++ b/app/api/files/[...path]/route.ts @@ -45,6 +45,9 @@ const isAllowedPath = (requestedPath: string): boolean => { 'vendor-evaluation', 'evaluation-attachments', 'vendor-attachments', + 'vendor-attachments/nda', + 'vendors/nda', + 'vendors', 'pq', 'pq/vendor' ]; diff --git a/app/api/upload/basicContract/chunk/route.ts b/app/api/upload/basicContract/chunk/route.ts index 383a8f36..db3d591e 100644 --- a/app/api/upload/basicContract/chunk/route.ts +++ b/app/api/upload/basicContract/chunk/route.ts @@ -33,83 +33,97 @@ export async function POST(request: NextRequest) { console.log(`📦 청크 저장 완료: ${chunkIndex + 1}/${totalChunks} (${buffer.length} bytes)`); // 마지막 청크인 경우 모든 청크를 합쳐 최종 파일 생성 - if (chunkIndex === totalChunks - 1) { - console.log(`🔄 파일 병합 시작: ${filename}`); - - try { - // 모든 청크를 순서대로 읽어서 병합 - const chunks: Buffer[] = []; - let totalSize = 0; - - for (let i = 0; i < totalChunks; i++) { - const chunkData = await readFile(path.join(tempDir, `chunk-${i}`)); - chunks.push(chunkData); - totalSize += chunkData.length; - } - - // 모든 청크를 하나의 Buffer로 병합 - const mergedBuffer = Buffer.concat(chunks, totalSize); - - const mergedFile = new File([mergedBuffer], filename, { - type: chunk.type || 'application/octet-stream', - lastModified: Date.now(), - }); - - const saveResult = await saveDRMFile( - mergedFile, - decryptWithServerAction, // 복호화 함수 - 'basicContract/template', // 저장 디렉토리 - // userId // 선택 - ); +// 마지막 청크인 경우 모든 청크를 합쳐 최종 파일 생성 +if (chunkIndex === totalChunks - 1) { + console.log(`🔄 파일 병합 시작: ${filename}`); + + try { + // 모든 청크를 순서대로 읽어서 병합 + const chunks: Buffer[] = []; + let totalSize = 0; + + for (let i = 0; i < totalChunks; i++) { + const chunkData = await readFile(path.join(tempDir, `chunk-${i}`)); + chunks.push(chunkData); + totalSize += chunkData.length; + } + + // 모든 청크를 하나의 Buffer로 병합 + const mergedBuffer = Buffer.concat(chunks, totalSize); + console.log(`📄 병합 완료: ${filename} (총 ${totalSize} bytes)`); + // 환경에 따른 저장 방식 선택 + const isProduction = process.env.NODE_ENV === 'production'; + let saveResult; - - console.log(`📄 병합 완료: ${filename} (총 ${totalSize} bytes)`); + if (isProduction) { + // Production: DRM 파일 처리 + console.log(`🔐 Production 환경 - DRM 파일 처리: ${filename}`); + + const mergedFile = new File([mergedBuffer], filename, { + type: chunk.type || 'application/octet-stream', + lastModified: Date.now(), + }); - // 공용 함수를 사용하여 파일 저장 - // const saveResult = await saveBuffer({ - // buffer: mergedBuffer, - // fileName: filename, - // directory: 'basicContract/template', - // originalName: filename - // }); + saveResult = await saveDRMFile( + mergedFile, + decryptWithServerAction, // 복호화 함수 + 'basicContract/template', // 저장 디렉토리 + // userId // 선택사항 + ); + } else { + // Development: 일반 파일 저장 + console.log(`🛠️ Development 환경 - 일반 파일 저장: ${filename}`); + + saveResult = await saveBuffer({ + buffer: mergedBuffer, + fileName: filename, + directory: 'basicContract/template', + originalName: filename + }); + } - // 임시 파일 정리 (비동기로 처리) - rm(tempDir, { recursive: true, force: true }) - .then(() => console.log(`🗑️ 임시 파일 정리 완료: ${fileId}`)) - .catch((e: unknown) => console.error('청크 정리 오류:', e)); + // 임시 파일 정리 (비동기로 처리) + rm(tempDir, { recursive: true, force: true }) + .then(() => console.log(`🗑️ 임시 파일 정리 완료: ${fileId}`)) + .catch((e: unknown) => console.error('청크 정리 오류:', e)); - if (saveResult.success) { - console.log(`✅ 최종 파일 저장 완료: ${saveResult.fileName}`); - - return NextResponse.json({ - success: true, - fileName: filename, - filePath: saveResult.publicPath, - hashedFileName: saveResult.fileName, - fileSize: totalSize - }); - } else { - console.error('파일 저장 실패:', saveResult.error); - return NextResponse.json({ - success: false, - error: saveResult.error || '파일 저장에 실패했습니다' - }, { status: 500 }); - } - - } catch (mergeError) { - console.error('파일 병합 오류:', mergeError); - - // 오류 발생 시 임시 파일 정리 - rm(tempDir, { recursive: true, force: true }) - .catch((e: unknown) => console.error('임시 파일 정리 오류:', e)); - - return NextResponse.json({ - success: false, - error: '파일 병합 중 오류가 발생했습니다' - }, { status: 500 }); - } + if (saveResult.success) { + const envPrefix = isProduction ? '🔐' : '🛠️'; + console.log(`${envPrefix} 최종 파일 저장 완료: ${saveResult.fileName}`); + + return NextResponse.json({ + success: true, + fileName: filename, + filePath: saveResult.publicPath, + hashedFileName: saveResult.fileName, + fileSize: totalSize, + environment: isProduction ? 'production' : 'development', + processingType: isProduction ? 'DRM' : 'standard' + }); + } else { + const envPrefix = isProduction ? '🔐' : '🛠️'; + console.error(`${envPrefix} 파일 저장 실패:`, saveResult.error); + return NextResponse.json({ + success: false, + error: saveResult.error || '파일 저장에 실패했습니다', + environment: isProduction ? 'production' : 'development' + }, { status: 500 }); } + + } catch (mergeError) { + console.error('파일 병합 오류:', mergeError); + + // 오류 발생 시 임시 파일 정리 + rm(tempDir, { recursive: true, force: true }) + .catch((e: unknown) => console.error('임시 파일 정리 오류:', e)); + + return NextResponse.json({ + success: false, + error: '파일 병합 중 오류가 발생했습니다' + }, { status: 500 }); + } +} return NextResponse.json({ success: true, -- cgit v1.2.3