diff options
| author | 0-Zz-ang <s1998319@gmail.com> | 2025-08-17 23:05:19 +0900 |
|---|---|---|
| committer | 0-Zz-ang <s1998319@gmail.com> | 2025-08-17 23:05:19 +0900 |
| commit | 5adc1df95f80fbec7a0b5bbee15448b10d5aec3a (patch) | |
| tree | c748910aacb05f13e335f9afa39eb9d763f88d7c /app | |
| parent | 6013fe51293ea067400e6b3b26691705608eba22 (diff) | |
(박서영)evcp/document-list-only, evcp/vendor-data 생성
Diffstat (limited to 'app')
8 files changed, 447 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/approval/document-list-only/layout.tsx b/app/[lng]/evcp/(evcp)/approval/document-list-only/layout.tsx new file mode 100644 index 00000000..17e78c0a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/approval/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 ( + <Shell className="gap-2"> + <VendorDocumentListClientEvcp> + {children} + </VendorDocumentListClientEvcp> + </Shell> + ) +} diff --git a/app/[lng]/evcp/(evcp)/approval/document-list-only/page.tsx b/app/[lng]/evcp/(evcp)/approval/document-list-only/page.tsx new file mode 100644 index 00000000..5b49a6ef --- /dev/null +++ b/app/[lng]/evcp/(evcp)/approval/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<SearchParams> +} + +// 문서 테이블 래퍼 컴포넌트 (전체 계약용) +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 ( + <DocumentStagesTable + promises={Promise.all([documentsPromise])} + contractId={-1} // 전체 계약을 의미 + projectType="plant" // 기본값으로 plant 사용 + /> + ) +} + +function TableLoadingSkeleton() { + return ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <Skeleton className="h-6 w-32" /> + <div className="flex items-center gap-2"> + <Skeleton className="h-8 w-20" /> + <Skeleton className="h-8 w-24" /> + </div> + </div> + <div className="rounded-md border"> + <div className="p-4"> + <div className="space-y-3"> + {Array.from({ length: 5 }).map((_, i) => ( + <div key={i} className="flex items-center space-x-4"> + <Skeleton className="h-4 w-4" /> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-4 w-48" /> + <Skeleton className="h-4 w-20" /> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-12" /> + </div> + ))} + </div> + </div> + </div> + </div> + ) +} + +// 메인 페이지 컴포넌트 +export default async function DocumentStagesManagementPage({ + searchParams +}: IndexPageProps) { + const resolvedSearchParams = await searchParams + + return ( + <div className="mx-auto"> + {/* 문서 테이블 */} + <Suspense fallback={<TableLoadingSkeleton />}> + <DocumentTableWrapper + searchParams={resolvedSearchParams} + /> + </Suspense> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/document-list-only/layout.tsx b/app/[lng]/evcp/(evcp)/document-list-only/layout.tsx new file mode 100644 index 00000000..17e78c0a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/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 ( + <Shell className="gap-2"> + <VendorDocumentListClientEvcp> + {children} + </VendorDocumentListClientEvcp> + </Shell> + ) +} diff --git a/app/[lng]/evcp/(evcp)/document-list-only/page.tsx b/app/[lng]/evcp/(evcp)/document-list-only/page.tsx new file mode 100644 index 00000000..5b49a6ef --- /dev/null +++ b/app/[lng]/evcp/(evcp)/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<SearchParams> +} + +// 문서 테이블 래퍼 컴포넌트 (전체 계약용) +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 ( + <DocumentStagesTable + promises={Promise.all([documentsPromise])} + contractId={-1} // 전체 계약을 의미 + projectType="plant" // 기본값으로 plant 사용 + /> + ) +} + +function TableLoadingSkeleton() { + return ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <Skeleton className="h-6 w-32" /> + <div className="flex items-center gap-2"> + <Skeleton className="h-8 w-20" /> + <Skeleton className="h-8 w-24" /> + </div> + </div> + <div className="rounded-md border"> + <div className="p-4"> + <div className="space-y-3"> + {Array.from({ length: 5 }).map((_, i) => ( + <div key={i} className="flex items-center space-x-4"> + <Skeleton className="h-4 w-4" /> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-4 w-48" /> + <Skeleton className="h-4 w-20" /> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-12" /> + </div> + ))} + </div> + </div> + </div> + </div> + ) +} + +// 메인 페이지 컴포넌트 +export default async function DocumentStagesManagementPage({ + searchParams +}: IndexPageProps) { + const resolvedSearchParams = await searchParams + + return ( + <div className="mx-auto"> + {/* 문서 테이블 */} + <Suspense fallback={<TableLoadingSkeleton />}> + <DocumentTableWrapper + searchParams={resolvedSearchParams} + /> + </Suspense> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx b/app/[lng]/evcp/(evcp)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx new file mode 100644 index 00000000..f69aa525 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/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 ( + <p className="text-red-500">해당 폼의 메타 정보를 불러올 수 없습니다. ENG 모드의 경우에는 SHI 관리자에게 폼 생성 요청을 하시기 바랍니다.</p> + ); + } + + // 8) 렌더링 + return ( + <div className="space-y-6"> + <DynamicTable + contractItemId={packageIdAsNumber} + formCode={formCode} + formId={formId} + columnsJSON={columns} + dataJSON={data} + projectId={Number(projectId)} + editableFieldsMap={editableFieldsMap} // 새로 추가 + mode={mode} // 모드 전달 + /> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendor-data/layout.tsx b/app/[lng]/evcp/(evcp)/vendor-data/layout.tsx new file mode 100644 index 00000000..7d00359c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/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 ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + 협력업체 데이터 입력 + </h2> + <InformationButton pagePath="partners/vendor-data" /> + </div> + {/* <p className="text-muted-foreground"> + 각종 Data 입력할 수 있습니다 + </p> */} + </div> + </div> + </div> + + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden flex-col md:flex"> + {projects.length === 0 ? ( + <div className="p-4 text-center text-sm text-muted-foreground"> + No projects found for this vendor. + </div> + ) : ( + <VendorDataContainer + projects={projects} + defaultLayout={defaultLayout} + defaultCollapsed={defaultCollapsed} + navCollapsedSize={4} + > + {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} + {children} + </VendorDataContainer> + )} + </div> + </section> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendor-data/page.tsx b/app/[lng]/evcp/(evcp)/vendor-data/page.tsx new file mode 100644 index 00000000..ddc21a2b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/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 ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">전체 계약 협력업체 데이터 대시보드</h3> + <p className="text-sm text-muted-foreground"> + 모든 계약의 협력업체 데이터를 확인하고 관리할 수 있습니다. + </p> + </div> + <Separator /> + <div className="grid gap-4"> + <div className="rounded-lg border p-4"> + <h4 className="text-sm font-medium">사용 방법</h4> + <p className="text-sm text-muted-foreground mt-1"> + 1. 왼쪽 사이드바에서 계약을 선택하세요.<br /> + 2. 선택한 계약의 패키지 항목을 클릭하세요.<br /> + 3. 패키지의 태그 정보를 확인하고 관리할 수 있습니다.<br /> + 4. 폼 항목을 클릭하여 칼럼 정보를 확인하고 관리할 수 있습니다. + </p> + </div> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendor-data/tag/[id]/page.tsx b/app/[lng]/evcp/(evcp)/vendor-data/tag/[id]/page.tsx new file mode 100644 index 00000000..7250732f --- /dev/null +++ b/app/[lng]/evcp/(evcp)/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<SearchParams> +} + +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 ( + <div className="space-y-6"> + <div> + <TagsTable promises={promises} selectedPackageId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file |
