summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/approval/document-list-only/layout.tsx17
-rw-r--r--app/[lng]/evcp/(evcp)/approval/document-list-only/page.tsx98
-rw-r--r--app/[lng]/evcp/(evcp)/document-list-only/layout.tsx17
-rw-r--r--app/[lng]/evcp/(evcp)/document-list-only/page.tsx98
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx79
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-data/layout.tsx67
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-data/page.tsx28
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-data/tag/[id]/page.tsx43
-rw-r--r--components/document-lists/vendor-doc-list-client-evcp.tsx35
-rw-r--r--components/vendor-data/sidebar.tsx15
-rw-r--r--components/vendor-data/vendor-data-container.tsx270
-rw-r--r--lib/vendor-data/services.ts28
-rw-r--r--lib/vendor-document-list/plant/document-stages-columns.tsx150
-rw-r--r--lib/vendor-document-list/plant/document-stages-service.ts37
-rw-r--r--lib/vendor-document-list/plant/document-stages-table.tsx37
15 files changed, 734 insertions, 285 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
diff --git a/components/document-lists/vendor-doc-list-client-evcp.tsx b/components/document-lists/vendor-doc-list-client-evcp.tsx
new file mode 100644
index 00000000..cc9b6804
--- /dev/null
+++ b/components/document-lists/vendor-doc-list-client-evcp.tsx
@@ -0,0 +1,35 @@
+"use client"
+import * as React from "react"
+
+import { InformationButton } from "@/components/information/information-button"
+
+interface VendorDocumentsClientEvcpProps {
+ children: React.ReactNode
+}
+
+export default function VendorDocumentListClientEvcp({
+ children,
+}: VendorDocumentsClientEvcpProps) {
+ return (
+ <>
+ {/* 상단 영역: 타이틀만 표시 */}
+ <div className="flex items-center justify-between">
+ {/* 왼쪽: 타이틀 & 설명 */}
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">전체 문서 리스트 관리</h2>
+ <InformationButton pagePath="evcp/document-list" />
+ </div>
+ <p className="text-muted-foreground">
+ 전체 계약 대상 문서를 관리하고 진행 상황을 추적할 수 있습니다.
+ </p>
+ </div>
+ </div>
+
+ {/* 문서 목록/테이블 영역 */}
+ <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow p-5">
+ {children}
+ </section>
+ </>
+ )
+}
diff --git a/components/vendor-data/sidebar.tsx b/components/vendor-data/sidebar.tsx
index 3805d216..a6b35a9d 100644
--- a/components/vendor-data/sidebar.tsx
+++ b/components/vendor-data/sidebar.tsx
@@ -27,8 +27,7 @@ interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
selectedProjectId: number | null
selectedContractId: number | null
onSelectPackage: (itemId: number) => void
- forms: FormInfo[]
- selectedForm: string | null
+ forms?: FormInfo[] // 선택적 속성으로 변경
onSelectForm: (formName: string) => void
isLoadingForms?: boolean
mode: "IM" | "ENG" // 새로 추가: 현재 선택된 모드
@@ -43,7 +42,7 @@ export function Sidebar({
selectedContractId,
onSelectPackage,
forms,
- selectedForm,
+ // selectedForm, // 사용되지 않음
onSelectForm,
isLoadingForms = false,
mode = "IM", // 기본값 설정
@@ -90,13 +89,9 @@ export function Sidebar({
* ---------------------------
*/
const handlePackageClick = (itemId: number) => {
- // 상위 컴포넌트 상태 업데이트
+ // 상위 컴포넌트 상태 업데이트만 수행
+ // 라우팅은 하지 않음 (프로젝트 선택 상태 유지)
onSelectPackage(itemId)
-
- // 해당 태그 페이지로 라우팅
- // 예: /vendor-data/tag/123
- const baseSegments = segments.slice(0, segments.indexOf("vendor-data") + 1).join("/")
- router.push(`/${baseSegments}/tag/${itemId}`)
}
/**
@@ -204,7 +199,7 @@ export function Sidebar({
<Skeleton className="h-8 w-full" />
</div>
))
- ) : forms.length === 0 ? (
+ ) : !forms || forms.length === 0 ? (
<p className="text-sm text-muted-foreground px-2">
(No forms loaded)
</p>
diff --git a/components/vendor-data/vendor-data-container.tsx b/components/vendor-data/vendor-data-container.tsx
index a549594c..3974b791 100644
--- a/components/vendor-data/vendor-data-container.tsx
+++ b/components/vendor-data/vendor-data-container.tsx
@@ -1,63 +1,42 @@
"use client"
import * as React from "react"
-import { TooltipProvider } from "@/components/ui/tooltip"
+import { usePathname, useRouter, useSearchParams, useParams } from "next/navigation"
+import { useAtom } from "jotai"
+import { selectedModeAtom } from "@/atoms"
+import { Sidebar } from "./sidebar"
+import { ProjectSwitcher } from "./project-swicher"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
import { cn } from "@/lib/utils"
-import { ProjectSwitcher } from "./project-swicher"
-import { Sidebar } from "./sidebar"
-import { usePathname, useRouter, useSearchParams, useParams } from "next/navigation"
-import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services"
import { Separator } from "@/components/ui/separator"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
-import { ScrollArea } from "@/components/ui/scroll-area"
import { Button } from "@/components/ui/button"
-import { FormInput } from "lucide-react"
-import { Skeleton } from "@/components/ui/skeleton"
-import { selectedModeAtom } from '@/atoms'
-import { useAtom } from 'jotai'
+import { TooltipProvider } from "@/components/ui/tooltip"
interface PackageData {
itemId: number
itemName: string
}
-interface ContractData {
- contractId: number
- contractName: string
- packages: PackageData[]
-}
-
-interface ProjectData {
- projectId: number
- projectCode: string
- projectName: string
- projectType: string
- contracts: ContractData[]
-}
-
interface VendorDataContainerProps {
- projects: ProjectData[]
+ projects: {
+ projectId: number
+ projectCode: string
+ projectName: string
+ projectType: string
+ contracts: {
+ contractId: number
+ contractNo: string
+ contractName: string
+ packages: PackageData[]
+ }[]
+ }[]
defaultLayout?: number[]
defaultCollapsed?: boolean
- navCollapsedSize: number
+ navCollapsedSize?: number
children: React.ReactNode
}
-function getTagIdFromPathname(path: string | null): number | null {
- if (!path) return null;
-
- // 태그 패턴 검사 (/tag/123)
- const tagMatch = path.match(/\/tag\/(\d+)/)
- if (tagMatch) return parseInt(tagMatch[1], 10)
-
- // 폼 패턴 검사 (/form/123/...)
- const formMatch = path.match(/\/form\/(\d+)/)
- if (formMatch) return parseInt(formMatch[1], 10)
-
- return null
-}
-
export function VendorDataContainer({
projects,
defaultLayout = [20, 80],
@@ -71,8 +50,6 @@ export function VendorDataContainer({
const params = useParams()
const currentLng = params?.lng as string || 'en'
- const tagIdNumber = getTagIdFromPathname(pathname)
-
// 기본 상태
const [selectedProjectId, setSelectedProjectId] = React.useState(projects[0]?.projectId || 0)
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
@@ -80,15 +57,12 @@ export function VendorDataContainer({
projects[0]?.contracts[0]?.contractId || 0
)
const [selectedPackageId, setSelectedPackageId] = React.useState<number | null>(null)
- const [formList, setFormList] = React.useState<FormInfo[]>([])
- const [selectedFormCode, setSelectedFormCode] = React.useState<string | null>(null)
- const [isLoadingForms, setIsLoadingForms] = React.useState(false)
// 현재 선택된 프로젝트/계약/패키지
const currentProject = projects.find((p) => p.projectId === selectedProjectId) ?? projects[0]
const currentContract = currentProject?.contracts.find((c) => c.contractId === selectedContractId)
?? currentProject?.contracts[0]
-
+
// 프로젝트 타입 확인 - ship인 경우 항상 ENG 모드
const isShipProject = currentProject?.projectType === "ship"
@@ -104,31 +78,6 @@ export function VendorDataContainer({
React.useEffect(() => {
setSelectedMode(initialMode as "IM" | "ENG")
}, [initialMode, setSelectedMode])
-
- const isTagOrFormRoute = pathname ? (pathname.includes("/tag/") || pathname.includes("/form/")) : false
- const currentPackageName = isTagOrFormRoute
- ? currentContract?.packages.find((pkg) => pkg.itemId === selectedPackageId)?.itemName || "None"
- : "None"
-
- // 폼 목록에서 고유한 폼 이름만 추출
- const formNames = React.useMemo(() => {
- return [...new Set(formList.map((form) => form.formName))]
- }, [formList])
-
- // URL에서 현재 폼 코드 추출
- const getCurrentFormCode = (path: string): string | null => {
- const segments = path.split("/").filter(Boolean)
- const formIndex = segments.indexOf("form")
- if (formIndex !== -1 && segments[formIndex + 2]) {
- return segments[formIndex + 2]
- }
- return null
- }
-
- const currentFormCode = React.useMemo(() => {
- return pathname ? getCurrentFormCode(pathname) : null
- }, [pathname])
-
// URL에서 모드가 변경되면 상태도 업데이트 (ship 프로젝트가 아닐 때만)
React.useEffect(() => {
@@ -152,23 +101,7 @@ export function VendorDataContainer({
}
}, [isShipProject, router])
- // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅
- React.useEffect(() => {
- if (!currentContract) return
-
- if (tagIdNumber) {
- setSelectedPackageId(tagIdNumber)
- } else {
- // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로
- if (currentContract.packages?.length) {
- setSelectedPackageId(currentContract.packages[0].itemId)
- } else {
- setSelectedPackageId(null)
- }
- }
- }, [tagIdNumber, currentContract])
-
- // (2) 프로젝트 변경 시 계약 초기화
+ // (1) 프로젝트 변경 시 계약 초기화
React.useEffect(() => {
if (currentProject?.contracts.length) {
setSelectedContractId(currentProject.contracts[0].contractId)
@@ -177,38 +110,6 @@ export function VendorDataContainer({
}
}, [currentProject])
- // (3) 패키지 ID와 모드가 변경될 때마다 폼 로딩
- React.useEffect(() => {
- const packageId = getTagIdFromPathname(pathname)
-
- if (packageId) {
- setSelectedPackageId(packageId)
-
- // URL에서 패키지 ID를 얻었을 때 즉시 폼 로드
- loadFormsList(packageId, selectedMode);
- } else if (currentContract?.packages?.length) {
- const firstPackageId = currentContract.packages[0].itemId;
- setSelectedPackageId(firstPackageId);
- loadFormsList(firstPackageId, selectedMode);
- }
- }, [pathname, currentContract, selectedMode])
-
- // 모드에 따른 폼 로드 함수
- const loadFormsList = async (packageId: number, mode: "IM" | "ENG") => {
- if (!packageId) return;
-
- setIsLoadingForms(true);
- try {
- const result = await getFormsByContractItemId(packageId, mode);
- setFormList(result.forms || []);
- } catch (error) {
- console.error(`폼 로딩 오류 (${mode} 모드):`, error);
- setFormList([]);
- } finally {
- setIsLoadingForms(false);
- }
- };
-
// 핸들러들
function handleSelectContract(projId: number, cId: number) {
setSelectedProjectId(projId)
@@ -217,67 +118,72 @@ export function VendorDataContainer({
function handleSelectPackage(itemId: number) {
setSelectedPackageId(itemId)
+
+ // partners와 동일하게: 패키지 선택 시 해당 페이지로 이동
+ if (itemId && pathname) {
+ // 더 안전한 URL 생성 로직
+ let baseSegments: string;
+ const vendorDataIndex = pathname.split("/").filter(Boolean).indexOf("vendor-data");
+
+ if (vendorDataIndex !== -1) {
+ baseSegments = pathname.split("/").filter(Boolean).slice(0, vendorDataIndex + 1).join("/");
+ } else {
+ // vendor-data가 없으면 기본 경로 사용
+ baseSegments = `${currentLng}/evcp/vendor-data`;
+ }
+
+ const targetUrl = `/${baseSegments}/tag/${itemId}?mode=${selectedMode}`;
+ router.push(targetUrl);
+ }
}
function handleSelectForm(formName: string) {
- const form = formList.find((f) => f.formName === formName)
- if (form) {
- setSelectedFormCode(form.formCode)
+ // partners와 동일하게: 폼 선택 시 해당 페이지로 이동
+ if (selectedPackageId && pathname) {
+ // 더 안전한 URL 생성 로직
+ let baseSegments: string;
+ const vendorDataIndex = pathname.split("/").filter(Boolean).indexOf("vendor-data");
+
+ if (vendorDataIndex !== -1) {
+ baseSegments = pathname.split("/").filter(Boolean).slice(0, vendorDataIndex + 1).join("/");
+ } else {
+ // vendor-data가 없으면 기본 경로 사용
+ baseSegments = `${currentLng}/evcp/vendor-data`;
+ }
+
+ const targetUrl = `/${baseSegments}/form/${selectedPackageId}/${formName}/${selectedProjectId}/${selectedContractId}?mode=${selectedMode}`;
+ router.push(targetUrl);
}
}
// 모드 변경 핸들러
-// 모드 변경 핸들러
-const handleModeChange = async (mode: "IM" | "ENG") => {
- // ship 프로젝트인 경우 모드 변경 금지
- if (isShipProject && mode !== "ENG") return;
-
- setSelectedMode(mode);
-
- // 모드가 변경될 때 자동 네비게이션
- if (currentContract?.packages?.length) {
- const firstPackageId = currentContract.packages[0].itemId;
+ const handleModeChange = async (mode: "IM" | "ENG") => {
+ // ship 프로젝트인 경우 모드 변경 금지
+ if (isShipProject && mode !== "ENG") return;
- if (mode === "IM") {
- // IM 모드: 첫 번째 패키지로 이동
- const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data") + 1).join("/");
- router.push(`/${currentLng}/${baseSegments}/tag/${firstPackageId}?mode=${mode}`);
- } else {
- // ENG 모드: 폼 목록을 먼저 로드
- setIsLoadingForms(true);
- try {
- const result = await getFormsByContractItemId(firstPackageId, mode);
- setFormList(result.forms || []);
+ setSelectedMode(mode);
+
+ // 모드가 변경될 때 자동 네비게이션
+ if (currentContract?.packages?.length) {
+ const firstPackageId = currentContract.packages[0].itemId;
+
+ if (pathname) {
+ // 더 안전한 URL 생성 로직
+ let baseSegments: string;
+ const vendorDataIndex = pathname.split("/").filter(Boolean).indexOf("vendor-data");
- // 폼이 있으면 첫 번째 폼으로 이동
- if (result.forms && result.forms.length > 0) {
- const firstForm = result.forms[0];
- setSelectedFormCode(firstForm.formCode);
-
- const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data") + 1).join("/");
- router.push(`/${currentLng}/${baseSegments}/form/0/${firstForm.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`);
+ if (vendorDataIndex !== -1) {
+ baseSegments = pathname.split("/").filter(Boolean).slice(0, vendorDataIndex + 1).join("/");
} else {
- // 폼이 없으면 모드만 변경
- const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data") + 1).join("/");
- router.push(`/${currentLng}/${baseSegments}/form/0/0/${selectedProjectId}/${selectedContractId}?mode=${mode}`);
+ // vendor-data가 없으면 기본 경로 사용
+ baseSegments = `${currentLng}/evcp/vendor-data`;
}
- } catch (error) {
- console.error(`폼 로딩 오류 (${mode} 모드):`, error);
- // 오류 발생 시 모드만 변경
- const url = new URL(window.location.href);
- url.searchParams.set('mode', mode);
- router.replace(url.pathname + url.search);
- } finally {
- setIsLoadingForms(false);
+
+ const targetUrl = `/${baseSegments}/tag/${firstPackageId}?mode=${mode}`;
+ router.push(targetUrl);
}
}
- } else {
- // 패키지가 없는 경우, 모드만 변경
- const url = new URL(window.location.href);
- url.searchParams.set('mode', mode);
- router.replace(url.pathname + url.search);
}
-};
return (
<TooltipProvider delayDuration={0}>
@@ -318,14 +224,7 @@ const handleModeChange = async (mode: "IM" | "ENG") => {
selectedProjectId={selectedProjectId}
selectedContractId={selectedContractId}
onSelectPackage={handleSelectPackage}
- forms={formList}
- selectedForm={
- selectedFormCode
- ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
- : null
- }
onSelectForm={handleSelectForm}
- isLoadingForms={isLoadingForms}
mode="ENG"
className="hidden lg:block"
/>
@@ -351,14 +250,7 @@ const handleModeChange = async (mode: "IM" | "ENG") => {
selectedContractId={selectedContractId}
selectedProjectId={selectedProjectId}
onSelectPackage={handleSelectPackage}
- forms={formList}
- selectedForm={
- selectedFormCode
- ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
- : null
- }
onSelectForm={handleSelectForm}
- isLoadingForms={isLoadingForms}
mode="IM"
className="hidden lg:block"
/>
@@ -372,14 +264,7 @@ const handleModeChange = async (mode: "IM" | "ENG") => {
selectedContractId={selectedContractId}
selectedProjectId={selectedProjectId}
onSelectPackage={handleSelectPackage}
- forms={formList}
- selectedForm={
- selectedFormCode
- ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
- : null
- }
onSelectForm={handleSelectForm}
- isLoadingForms={isLoadingForms}
mode="ENG"
className="hidden lg:block"
/>
@@ -418,14 +303,7 @@ const handleModeChange = async (mode: "IM" | "ENG") => {
selectedProjectId={selectedProjectId}
selectedContractId={selectedContractId}
onSelectPackage={handleSelectPackage}
- forms={formList}
- selectedForm={
- selectedFormCode
- ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
- : null
- }
onSelectForm={handleSelectForm}
- isLoadingForms={isLoadingForms}
mode={isShipProject ? "ENG" : selectedMode}
className="hidden lg:block"
/>
@@ -441,7 +319,7 @@ const handleModeChange = async (mode: "IM" | "ENG") => {
<h2 className="text-lg font-bold">
{isShipProject || selectedMode === "ENG"
? "Engineering Mode"
- : `Package: ${currentPackageName}`}
+ : `Package: ${currentContract?.packages.find((pkg) => pkg.itemId === selectedPackageId)?.itemName || "None"}`}
</h2>
</div>
{children}
diff --git a/lib/vendor-data/services.ts b/lib/vendor-data/services.ts
index 0ec935b9..e8ecd01c 100644
--- a/lib/vendor-data/services.ts
+++ b/lib/vendor-data/services.ts
@@ -3,11 +3,10 @@
import db from "@/db/db"
import { items } from "@/db/schema/items"
import { projects } from "@/db/schema/projects"
-import { Tag, tags } from "@/db/schema/vendorData"
import { eq } from "drizzle-orm"
-import { revalidateTag, unstable_noStore } from "next/cache";
-import { unstable_cache } from "@/lib/unstable-cache";
import { contractItems, contracts } from "@/db/schema/contract";
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
export interface ProjectWithContracts {
projectId: number
@@ -20,16 +19,22 @@ export interface ProjectWithContracts {
contractNo: string
contractName: string
packages: {
- itemId: number
+ itemId: number // contract_items.id
itemName: string
}[]
}[]
}
export async function getVendorProjectsAndContracts(
- vendorId: number
+ vendorId?: number
): Promise<ProjectWithContracts[]> {
- const rows = await db
+ // 세션에서 도메인 정보 가져오기
+ const session = await getServerSession(authOptions)
+
+ // EVCP 도메인일 때만 전체 조회
+ const isEvcpDomain = session?.user?.domain === "evcp"
+
+ const query = db
.select({
projectId: projects.id,
projectCode: projects.code,
@@ -47,7 +52,12 @@ export async function getVendorProjectsAndContracts(
.innerJoin(projects, eq(contracts.projectId, projects.id))
.innerJoin(contractItems, eq(contractItems.contractId, contracts.id))
.innerJoin(items, eq(contractItems.itemId, items.id))
- .where(eq(contracts.vendorId, vendorId))
+
+ if (!isEvcpDomain && vendorId) {
+ query.where(eq(contracts.vendorId, vendorId))
+ }
+
+ const rows = await query
const projectMap = new Map<number, ProjectWithContracts>()
@@ -71,6 +81,7 @@ export async function getVendorProjectsAndContracts(
(c) => c.contractId === row.contractId
)
if (!contractEntry) {
+
// 새 계약 항목
contractEntry = {
contractId: row.contractId,
@@ -97,4 +108,5 @@ export async function getVendorProjectsAndContracts(
}
return Array.from(projectMap.values())
-} \ No newline at end of file
+}
+
diff --git a/lib/vendor-document-list/plant/document-stages-columns.tsx b/lib/vendor-document-list/plant/document-stages-columns.tsx
index 742b8a8a..d4baf7fb 100644
--- a/lib/vendor-document-list/plant/document-stages-columns.tsx
+++ b/lib/vendor-document-list/plant/document-stages-columns.tsx
@@ -35,6 +35,7 @@ import { cn } from "@/lib/utils"
interface GetColumnsProps {
setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<DocumentStagesOnlyView> | null>>
projectType: string
+ domain?: "evcp" | "partners" // 선택적 파라미터로 유지
}
// 유틸리티 함수들
@@ -76,7 +77,7 @@ const getPriorityText = (priority: string) => {
case 'HIGH': return 'High'
case 'MEDIUM': return 'Medium'
case 'LOW': return 'Low'
- default: priority
+ default: return priority
}
}
@@ -136,9 +137,11 @@ const DueDateInfo = ({
export function getDocumentStagesColumns({
setRowAction,
- projectType
+ projectType,
+ domain = "partners", // 기본값 설정
}: GetColumnsProps): ColumnDef<DocumentStagesOnlyView>[] {
const isPlantProject = projectType === "plant"
+ const isEvcpDomain = domain === "evcp"
const columns: ColumnDef<DocumentStagesOnlyView>[] = [
// 체크박스 선택
@@ -167,7 +170,11 @@ export function getDocumentStagesColumns({
enableSorting: false,
enableHiding: false,
},
- {
+ ]
+
+ // EVCP 도메인일 때만 Project 컬럼 추가 (앞쪽으로 이동)
+ if (isEvcpDomain) {
+ columns.push({
accessorKey: "projectCode",
header: ({ column }) => (
<DataTableColumnHeaderSimple column={column} title="Project" />
@@ -183,57 +190,101 @@ export function getDocumentStagesColumns({
meta: {
excelHeader: "Project"
},
- },
-
-
+ })
+ }
- // 문서번호
- {
- accessorKey: "docNumber",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Document Number" />
- ),
- cell: ({ row }) => {
- const doc = row.original
- return (
- <span className="font-mono text-sm font-medium">{doc.docNumber}</span>
- )
- },
- size: 140,
- enableResizing: true,
- meta: {
- excelHeader: "Document Number"
+ // EVCP 도메인일 때만 Vendor 정보 컬럼들 추가 (앞쪽으로 이동)
+ if (isEvcpDomain) {
+ columns.push(
+ // Vendor Code
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Vendor Code" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ return doc.vendorCode ? (
+ <span className="font-mono text-sm font-medium text-blue-600">{doc.vendorCode}</span>
+ ) : (
+ <span className="text-gray-400 text-sm">-</span>
+ )
+ },
+ size: 120,
+ enableResizing: true,
+ meta: {
+ excelHeader: "Vendor Code"
+ },
},
- },
+ // Vendor Name
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Vendor Name" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ return doc.vendorName ? (
+ <span className="text-sm font-medium">{doc.vendorName}</span>
+ ) : (
+ <span className="text-gray-400 text-sm">-</span>
+ )
+ },
+ size: 150,
+ enableResizing: true,
+ meta: {
+ excelHeader: "Vendor Name"
+ },
+ }
+ )
+ }
- // 문서명 (PIC 포함)
- {
- accessorKey: "title",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Document Name" />
- ),
- cell: ({ row }) => {
- const doc = row.original
- return (
- <div className="min-w-0 flex-1">
- <div className="font-medium text-gray-900 truncate text-sm" title={doc.title}>
- {doc.title}
- </div>
- {doc.pic && (
- <span className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded mt-1 inline-block">
- PIC: {doc.pic}
- </span>
- )}
+ // 문서번호
+ columns.push({
+ accessorKey: "docNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Document Number" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ return (
+ <span className="font-mono text-sm font-medium">{doc.docNumber}</span>
+ )
+ },
+ size: 140,
+ enableResizing: true,
+ meta: {
+ excelHeader: "Document Number"
+ },
+ })
+
+ // 문서명 (PIC 포함)
+ columns.push({
+ accessorKey: "title",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Document Name" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ return (
+ <div className="min-w-0 flex-1">
+ <div className="font-medium text-gray-900 truncate text-sm" title={doc.title}>
+ {doc.title}
</div>
- )
- },
- size: 220,
- enableResizing: true,
- meta: {
- excelHeader: "Document Name"
- },
+ {doc.pic && (
+ <span className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded mt-1 inline-block">
+ PIC: {doc.pic}
+ </span>
+ )}
+ </div>
+ )
},
- ]
+ size: 220,
+ enableResizing: true,
+ meta: {
+ excelHeader: "Document Name"
+ },
+ })
// Plant 프로젝트용 추가 컬럼들
if (isPlantProject) {
@@ -258,7 +309,6 @@ export function getDocumentStagesColumns({
excelHeader: "Vendor Doc No."
},
},
-
)
}
diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts
index c6a891c8..57f17bae 100644
--- a/lib/vendor-document-list/plant/document-stages-service.ts
+++ b/lib/vendor-document-list/plant/document-stages-service.ts
@@ -30,6 +30,8 @@ import { unstable_noStore as noStore } from "next/cache"
import { filterColumns } from "@/lib/filter-columns"
import { GetEnhancedDocumentsSchema, GetDocumentsSchema } from "../enhanced-document-service"
import { countDocumentStagesOnly, selectDocumentStagesOnly } from "../repository"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
interface UpdateDocumentData {
documentId: number
@@ -1013,7 +1015,6 @@ export async function getDocumentStagesOnly(
) {
try {
const offset = (input.page - 1) * input.perPage
-
// 고급 필터 처리
const advancedWhere = filterColumns({
table: stageDocumentsView,
@@ -1034,21 +1035,39 @@ export async function getDocumentStagesOnly(
)
}
- // 최종 WHERE 조건
- const finalWhere = and(
- advancedWhere,
- globalWhere,
- eq(stageDocumentsView.contractId, contractId)
- )
- // 정렬 처리
+ // 세션에서 도메인 정보 가져오기
+ const session = await getServerSession(authOptions)
+ const isEvcpDomain = session?.user?.domain === "evcp"
+
+ // 도메인별 WHERE 조건 설정
+ let finalWhere
+ if (isEvcpDomain) {
+ // EVCP: 전체 계약 조회 (contractId 조건 제거)
+ finalWhere = and(
+ advancedWhere,
+ globalWhere
+ )
+ } else {
+ // Partners: 특정 계약 조회
+ finalWhere = and(
+ advancedWhere,
+ globalWhere,
+ eq(documentStagesOnlyView.contractId, contractId)
+ )
+ }
+
+
+
+ // 정렬 처리
const orderBy = input.sort && input.sort.length > 0
? input.sort.map((item) =>
item.desc
? desc(stageDocumentsView[item.id])
: asc(stageDocumentsView[item.id])
)
- : [desc(stageDocumentsView.createdAt)]
+ : [desc(documentStagesOnlyView.createdAt)]
+
// 트랜잭션 실행
const { data, total } = await db.transaction(async (tx) => {
diff --git a/lib/vendor-document-list/plant/document-stages-table.tsx b/lib/vendor-document-list/plant/document-stages-table.tsx
index ccf35f4b..8bfae284 100644
--- a/lib/vendor-document-list/plant/document-stages-table.tsx
+++ b/lib/vendor-document-list/plant/document-stages-table.tsx
@@ -34,6 +34,7 @@ import { EditDocumentDialog } from "./document-stage-dialogs"
import { EditStageDialog } from "./document-stage-dialogs"
import { ExcelImportDialog } from "./document-stage-dialogs"
import { DocumentsTableToolbarActions } from "./document-stage-toolbar"
+import { useSession } from "next-auth/react"
interface DocumentStagesTableProps {
promises: Promise<[Awaited<ReturnType<typeof getDocumentStagesOnly>>]>
@@ -46,13 +47,18 @@ export function DocumentStagesTable({
contractId,
projectType,
}: DocumentStagesTableProps) {
- const [{ data, pageCount, total }] = React.use(promises)
+ const [{ data, pageCount }] = React.use(promises)
+
+ const { data: session } = useSession()
+
// URL에서 언어 파라미터 가져오기
const params = useParams()
const lng = (params?.lng as string) || 'ko'
const { t } = useTranslation(lng, 'document')
+ // 세션에서 도메인을 가져오기
+ const currentDomain = session?.user?.domain as "evcp" | "partners"
// 상태 관리
const [rowAction, setRowAction] = React.useState<DataTableRowAction<DocumentStagesOnlyView> | null>(null)
@@ -100,27 +106,31 @@ export function DocumentStagesTable({
}
}
},
- projectType
+ projectType,
+ domain: currentDomain
}),
- [expandedRows, projectType]
+ [expandedRows, projectType, currentDomain]
)
// 통계 계산
const stats = React.useMemo(() => {
- const totalDocs = data.length
- const overdue = data.filter(doc => doc.isOverdue).length
- const dueSoon = data.filter(doc =>
+ console.log('DocumentStagesTable - data:', data)
+ console.log('DocumentStagesTable - data length:', data?.length)
+
+ const totalDocs = data?.length || 0
+ const overdue = data?.filter(doc => doc.isOverdue)?.length || 0
+ const dueSoon = data?.filter(doc =>
doc.daysUntilDue !== null &&
doc.daysUntilDue >= 0 &&
doc.daysUntilDue <= 3
- ).length
- const inProgress = data.filter(doc => doc.currentStageStatus === 'IN_PROGRESS').length
- const highPriority = data.filter(doc => doc.currentStagePriority === 'HIGH').length
+ )?.length || 0
+ const inProgress = data?.filter(doc => doc.currentStageStatus === 'IN_PROGRESS')?.length || 0
+ const highPriority = data?.filter(doc => doc.currentStagePriority === 'HIGH')?.length || 0
const avgProgress = totalDocs > 0
- ? Math.round(data.reduce((sum, doc) => sum + (doc.progressPercentage || 0), 0) / totalDocs)
+ ? Math.round((data?.reduce((sum, doc) => sum + (doc.progressPercentage || 0), 0) || 0) / totalDocs)
: 0
- return {
+ const result = {
total: totalDocs,
overdue,
dueSoon,
@@ -128,6 +138,9 @@ export function DocumentStagesTable({
highPriority,
avgProgress
}
+
+ console.log('DocumentStagesTable - stats:', result)
+ return result
}, [data])
// 빠른 필터링
@@ -273,7 +286,7 @@ export function DocumentStagesTable({
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground">
- {t('documentList.dashboard.totalDocumentCount', { total })}
+ {t('documentList.dashboard.totalDocumentCount', { total: stats.total })}
</p>
</CardContent>
</Card>