diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-27 17:53:34 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-27 17:53:34 +0900 |
| commit | 5870b73785715d1585531e655c06d8c068eb64ac (patch) | |
| tree | 1d19e1482f5210cc56e778158b51e810f9717c46 | |
| parent | 95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff) | |
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
43 files changed, 1755 insertions, 5482 deletions
diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx deleted file mode 100644 index 351fbca3..00000000 --- a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx -import DynamicTable from "@/components/form-data-plant/form-data-table"; -import { getFormData, getFormId,getProjectIdByCode } from "@/lib/forms-plant/services"; -import { useTranslation } from "@/i18n"; -import { Skeleton } from "@/components/ui/skeleton"; - -interface EngineeringFormPageProps { - params: { - lng: string; - projectCode: string; - packageCode: string; - formCode: string; - }; - searchParams?: { - mode?: string; - }; -} - -export default async function EngineeringFormPage({ - params, - searchParams, -}: EngineeringFormPageProps) { - // 1) 구조 분해 할당 - const resolvedParams = await params; - - // 2) searchParams도 await 필요 - const resolvedSearchParams = await searchParams; - - // 3) 구조 분해 할당 - const { lng, projectCode, packageCode, formCode } = resolvedParams; - - // i18n 설정 - const { t } = await useTranslation(lng, 'engineering'); - - // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) - const mode = "ENG"; // 기본값은 IM - - // 4) DB 조회 - projectCode와 packageCode를 전달 - const { columns, data, editableFieldsMap } = await getFormData( - formCode, - projectCode, - packageCode - ); - - // 5) formId 조회 - projectCode와 packageCode를 전달 - const { formId } = await getFormId(projectCode, packageCode, formCode, mode); - - const projectId = await getProjectIdByCode(projectCode) - - // 6) 예외 처리 - if (!columns) { - return ( - <p className="text-red-500"> - {t('errors.form_meta_not_found')} - </p> - ); - } - - // 7) 렌더링 - return ( - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <h3 className="text-lg font-semibold">Engineering Form</h3> - <p className="text-sm text-muted-foreground"> - Project: {projectCode} / Package: {packageCode} / Form: {formCode} - </p> - </div> - </div> - - <div className="space-y-6"> - <DynamicTable - projectId={projectId} - projectCode={projectCode} - packageCode={packageCode} - formCode={formCode} - formId={formId} - columnsJSON={columns} - dataJSON={data} - editableFieldsMap={editableFieldsMap} - mode={"ENG"} - /> - </div> - </div> - ); -} - -function TableSkeleton() { - return ( - <div className="space-y-4"> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-[400px] w-full" /> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx deleted file mode 100644 index 29188061..00000000 --- a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx -import DynamicTable from "@/components/form-data-plant/form-data-table"; -import { getFormData, getFormId, getProjectIdByCode } from "@/lib/forms-plant/services"; -import { useTranslation } from "@/i18n"; -import { Skeleton } from "@/components/ui/skeleton"; - -interface EngineeringFormPageProps { - params: { - lng: string; - projectCode: string; - packageCode: string; - formCode: string; - }; - searchParams?: { - mode?: string; - }; -} - -export default async function IMFormPage({ - params, - searchParams, -}: EngineeringFormPageProps) { - // 1) 구조 분해 할당 - const resolvedParams = await params; - - // 2) searchParams도 await 필요 - const resolvedSearchParams = await searchParams; - - // 3) 구조 분해 할당 - const { lng, projectCode, packageCode, formCode } = resolvedParams; - - // i18n 설정 - const { t } = await useTranslation(lng, 'engineering'); - - // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) - const mode = "IM"; // 기본값은 IM - - // 4) DB 조회 - projectCode와 packageCode를 전달 - const { columns, data, editableFieldsMap } = await getFormData( - formCode, - projectCode, - packageCode - ); - - // 5) formId 조회 - projectCode와 packageCode를 전달 - const { formId } = await getFormId(projectCode, packageCode, formCode, mode); - - const projectId = await getProjectIdByCode(projectCode) - - // 6) 예외 처리 - if (!columns) { - return ( - <p className="text-red-500"> - {t('errors.form_meta_not_found')} - </p> - ); - } - - // 7) 렌더링 - return ( - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <h3 className="text-lg font-semibold">Engineering Form</h3> - <p className="text-sm text-muted-foreground"> - Project: {projectCode} / Package: {packageCode} / Form: {formCode} - </p> - </div> - </div> - - <div className="space-y-6"> - <DynamicTable - projectId={projectId} - projectCode={projectCode} - packageCode={packageCode} - formCode={formCode} - formId={formId} - columnsJSON={columns} - dataJSON={data} - editableFieldsMap={editableFieldsMap} - mode={"IM"} - /> - </div> - </div> - ); -} - -function TableSkeleton() { - return ( - <div className="space-y-4"> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-[400px] w-full" /> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx deleted file mode 100644 index 4904a8ff..00000000 --- a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/page.tsx - -import { TagsTable } from "@/lib/tags-plant/table/tag-table" - -interface MasterTagListPageProps { - params: Promise<{ - lng: string - projectCode: string - packageCode: string - }> -} - -export default async function MasterTagListPage({ - params, -}: MasterTagListPageProps) { - const { projectCode, packageCode } = await params - - return ( - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <h3 className="text-lg font-semibold">Master Tag List</h3> - <p className="text-sm text-muted-foreground"> - Project: {projectCode} / Package: {packageCode} - </p> - </div> - </div> - - {/* 완전 클라이언트 컴포넌트 */} - <TagsTable - projectCode={projectCode} - packageCode={packageCode} - formCode="MASTER" - /> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx index 792a3a6a..8a9c43e9 100644 --- a/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx +++ b/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx @@ -2,35 +2,41 @@ import * as React from "react" import { cookies } from "next/headers" import { Shell } from "@/components/shell" +import { getVendorProjectsAndContracts } from "@/lib/vendor-data-plant/services" import { VendorDataContainer } from "@/components/vendor-data-plant/vendor-data-container" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { getServerSession } from "next-auth" import { InformationButton } from "@/components/information/information-button" import { useTranslation } from "@/i18n" -import { getVendorProjectsWithPackages } from "@/lib/vendor-data/services" interface VendorDataLayoutProps { children: React.ReactNode params: { lng?: string } } +// Layout 컴포넌트는 서버 컴포넌트입니다 export default async function VendorDataLayout({ children, params, }: VendorDataLayoutProps) { - const { lng } = await params + // 기본 언어는 'ko'로 설정, params.locale이 있으면 사용 + const { lng } = await params; const language = lng || 'en' const { t } = await useTranslation(language, 'engineering') const session = await getServerSession(authOptions) const vendorId = session?.user.companyId + // const vendorId = "17" const idAsNumber = Number(vendorId) - // 프로젝트 및 패키지 데이터 가져오기 - const projects = await getVendorProjectsWithPackages(idAsNumber, "plant") + // 프로젝트 데이터 가져오기 (type=plant만) + const projects = await getVendorProjectsAndContracts(idAsNumber, "plant") // 레이아웃 설정 쿠키 가져오기 + // 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") @@ -48,6 +54,9 @@ export default async function VendorDataLayout({ </h2> <InformationButton pagePath="partners/vendor-data-plant" /> </div> + {/* <p className="text-muted-foreground"> + 각종 Data 입력할 수 있습니다 + </p> */} </div> </div> </div> @@ -65,6 +74,7 @@ export default async function VendorDataLayout({ defaultCollapsed={defaultCollapsed} navCollapsedSize={4} > + {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} {children} </VendorDataContainer> )} diff --git a/app/api/cron/form-tags-plant/start/route.ts b/app/api/cron/form-tags-plant/start/route.ts deleted file mode 100644 index 17eb8979..00000000 --- a/app/api/cron/form-tags-plant/start/route.ts +++ /dev/null @@ -1,141 +0,0 @@ -// app/api/cron/form-tags/start/route.ts -import { NextRequest } from 'next/server'; -import { v4 as uuidv4 } from 'uuid'; -import { revalidateTag } from 'next/cache'; - -// 동기화 작업의 상태를 저장할 Map -// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다 -const syncJobs = new Map<string, { - status: 'queued' | 'processing' | 'completed' | 'failed'; - startTime: Date; - endTime?: Date; - result?: any; - error?: string; - progress?: number; - projectCode?: string; - formCode?: string; - packageCode?: string; - mode?: string -}>(); - -export async function POST(request: NextRequest) { - try { - // 요청 데이터 가져오기 - let projectCode: string | undefined; - let formCode: string | undefined; - let packageCode: string | undefined; - let mode: string | undefined; - - - const body = await request.json(); - projectCode = body.projectCode; - formCode = body.formCode; - packageCode = body.packageCode; - mode = body.mode; // 모드 정보 추출 - - - // 고유 ID 생성 - const syncId = uuidv4(); - - // 작업 상태 초기화 - syncJobs.set(syncId, { - status: 'queued', - startTime: new Date(), - formCode, - projectCode, - packageCode, - mode - - }); - - // 비동기 작업 시작 (백그라운드에서 실행) - processTagImport(syncId).catch(error => { - console.error('Background tag import job failed:', error); - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - status: 'failed', - endTime: new Date(), - error: error.message || 'Unknown error occurred' - }); - }); - - // 즉시 응답 반환 (작업 ID 포함) - return Response.json({ - success: true, - message: 'Tag import job started', - syncId - }, { status: 200 }); - - } catch (error: any) { - console.error('Failed to start tag import job:', error); - return Response.json({ - success: false, - error: error.message || 'Failed to start tag import job' - }, { status: 500 }); - } -} - -// 백그라운드에서 실행되는 태그 가져오기 작업 -async function processTagImport(syncId: string) { - try { - const jobInfo = syncJobs.get(syncId)!; - const formCode = jobInfo.formCode; - const projectCode = jobInfo.projectCode; - const packageCode = jobInfo.packageCode || 0; - const mode = jobInfo.mode || 0; - - // 상태 업데이트: 처리 중 - syncJobs.set(syncId, { - ...jobInfo, - status: 'processing', - progress: 0, - }); - - if (!formCode || !projectCode ) { - throw new Error('formCode,projectCode is required'); - } - - // 여기서 실제 태그 가져오기 로직 import - const { importTagsFromSEDP } = await import('@/lib/sedp/get-form-tags-plant'); - - // 진행 상황 업데이트를 위한 콜백 함수 - const updateProgress = (progress: number) => { - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - progress - }); - }; - - // 실제 태그 가져오기 실행 - const result = await importTagsFromSEDP(formCode, projectCode, packageCode, updateProgress); - - // 명시적으로 캐시 무효화 - revalidateTag(`forms-${packageCode}-${mode}`); - - // 상태 업데이트: 완료 - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - status: 'completed', - endTime: new Date(), - result, - progress: 100, - }); - - return result; - } catch (error: any) { - // 에러 발생 시 상태 업데이트 - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - status: 'failed', - endTime: new Date(), - error: error.message || 'Unknown error occurred', - }); - - throw error; // 에러 다시 던지기 - } -} - -// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용) -export function getSyncJobStatus(id: string) { - return syncJobs.get(id); -}
\ No newline at end of file diff --git a/app/api/cron/form-tags-plant/status/route.ts b/app/api/cron/form-tags-plant/status/route.ts deleted file mode 100644 index 9d288f52..00000000 --- a/app/api/cron/form-tags-plant/status/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -// app/api/cron/tags/status/route.ts -import { NextRequest } from 'next/server'; -import { getSyncJobStatus } from '../start/route'; - -export async function GET(request: NextRequest) { - try { - // URL에서 작업 ID 가져오기 - const searchParams = request.nextUrl.searchParams; - const syncId = searchParams.get('id'); - - if (!syncId) { - return Response.json({ - success: false, - error: 'Missing sync ID parameter' - }, { status: 400 }); - } - - // 작업 상태 조회 - const jobStatus = getSyncJobStatus(syncId); - - if (!jobStatus) { - return Response.json({ - success: false, - error: 'Sync job not found' - }, { status: 404 }); - } - - // 작업 상태 반환 - return Response.json({ - success: true, - status: jobStatus.status, - startTime: jobStatus.startTime, - endTime: jobStatus.endTime, - progress: jobStatus.progress, - result: jobStatus.result, - error: jobStatus.error - }, { status: 200 }); - - } catch (error: any) { - console.error('Error retrieving tag import status:', error); - return Response.json({ - success: false, - error: error.message || 'Failed to retrieve tag import status' - }, { status: 500 }); - } -}
\ No newline at end of file diff --git a/app/api/cron/tags-plant/start/route.ts b/app/api/cron/tags-plant/start/route.ts deleted file mode 100644 index 83e06935..00000000 --- a/app/api/cron/tags-plant/start/route.ts +++ /dev/null @@ -1,149 +0,0 @@ -// app/api/cron/tags/start/route.ts -import { NextRequest } from 'next/server'; -import { v4 as uuidv4 } from 'uuid'; -import { revalidateTag } from 'next/cache'; - -// 동기화 작업의 상태를 저장할 Map -// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다 -const syncJobs = new Map<string, { - status: 'queued' | 'processing' | 'completed' | 'failed'; - startTime: Date; - endTime?: Date; - result?: any; - error?: string; - progress?: number; - projectCode?: string; - packageCode?: string; - mode?: string -}>(); - -export async function POST(request: NextRequest) { - try { - // 요청 데이터 가져오기 - let projectCode: string | undefined; - let packageCode: string | undefined; - let mode: string | undefined; - - try { - const body = await request.json(); - projectCode = body.projectCode; - packageCode = body.packageCode; - mode = body.mode; // 모드 정보 추출 - - } catch (error) { - // 요청 본문이 없거나 JSON이 아닌 경우, URL 파라미터 확인 - const searchParams = request.nextUrl.searchParams; - const projectCodeParam = searchParams.get('projectCode'); - const packageCodeParam = searchParams.get('packageCode'); - - if (projectCodeParam&&packageCodeParam) { - projectCode = projectCodeParam; - packageCode = packageCodeParam; - } - mode = searchParams.get('mode') || undefined; - } - - // 고유 ID 생성 - const syncId = uuidv4(); - - // 작업 상태 초기화 - syncJobs.set(syncId, { - status: 'queued', - startTime: new Date(), - projectCode, - packageCode, - mode - }); - - // 비동기 작업 시작 (백그라운드에서 실행) - processTagImport(syncId).catch(error => { - console.error('Background tag import job failed:', error); - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - status: 'failed', - endTime: new Date(), - error: error.message || 'Unknown error occurred' - }); - }); - - // 즉시 응답 반환 (작업 ID 포함) - return Response.json({ - success: true, - message: 'Tag import job started', - syncId - }, { status: 200 }); - - } catch (error: any) { - console.error('Failed to start tag import job:', error); - return Response.json({ - success: false, - error: error.message || 'Failed to start tag import job' - }, { status: 500 }); - } -} - -// 백그라운드에서 실행되는 태그 가져오기 작업 -async function processTagImport(syncId: string) { - try { - const jobInfo = syncJobs.get(syncId)!; - const projectCode = jobInfo.projectCode; - const packageCode = jobInfo.packageCode; - const mode = jobInfo.mode; // 모드 정보 추출 - - - // 상태 업데이트: 처리 중 - syncJobs.set(syncId, { - ...jobInfo, - status: 'processing', - progress: 0, - }); - - if (!packageCode) { - throw new Error('Package is required'); - } - - // 여기서 실제 태그 가져오기 로직 import - const { importTagsFromSEDP } = await import('@/lib/sedp/get-tags-plant'); - - // 진행 상황 업데이트를 위한 콜백 함수 - const updateProgress = (progress: number) => { - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - progress - }); - }; - - // 실제 태그 가져오기 실행 - const result = await importTagsFromSEDP(projectCode, packageCode,updateProgress, mode); - - // 명시적으로 캐시 무효화 - revalidateTag(`tags-${packageCode}`); - revalidateTag(`forms-${packageCode}-${mode}`); - - // 상태 업데이트: 완료 - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - status: 'completed', - endTime: new Date(), - result, - progress: 100, - }); - - return result; - } catch (error: any) { - // 에러 발생 시 상태 업데이트 - syncJobs.set(syncId, { - ...syncJobs.get(syncId)!, - status: 'failed', - endTime: new Date(), - error: error.message || 'Unknown error occurred', - }); - - throw error; // 에러 다시 던지기 - } -} - -// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용) -export function getSyncJobStatus(id: string) { - return syncJobs.get(id); -}
\ No newline at end of file diff --git a/app/api/cron/tags-plant/status/route.ts b/app/api/cron/tags-plant/status/route.ts deleted file mode 100644 index 9d288f52..00000000 --- a/app/api/cron/tags-plant/status/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -// app/api/cron/tags/status/route.ts -import { NextRequest } from 'next/server'; -import { getSyncJobStatus } from '../start/route'; - -export async function GET(request: NextRequest) { - try { - // URL에서 작업 ID 가져오기 - const searchParams = request.nextUrl.searchParams; - const syncId = searchParams.get('id'); - - if (!syncId) { - return Response.json({ - success: false, - error: 'Missing sync ID parameter' - }, { status: 400 }); - } - - // 작업 상태 조회 - const jobStatus = getSyncJobStatus(syncId); - - if (!jobStatus) { - return Response.json({ - success: false, - error: 'Sync job not found' - }, { status: 404 }); - } - - // 작업 상태 반환 - return Response.json({ - success: true, - status: jobStatus.status, - startTime: jobStatus.startTime, - endTime: jobStatus.endTime, - progress: jobStatus.progress, - result: jobStatus.result, - error: jobStatus.error - }, { status: 200 }); - - } catch (error: any) { - console.error('Error retrieving tag import status:', error); - return Response.json({ - success: false, - error: error.message || 'Failed to retrieve tag import status' - }, { status: 500 }); - } -}
\ No newline at end of file diff --git a/components/client-data-table/data-table.tsx b/components/client-data-table/data-table.tsx index 371a1dab..3e009302 100644 --- a/components/client-data-table/data-table.tsx +++ b/components/client-data-table/data-table.tsx @@ -49,9 +49,8 @@ interface DataTableProps<TData, TValue> { children?: React.ReactNode /** 선택 상태 초기화 트리거 */ clearSelection?: boolean - initialColumnPinning?: ColumnPinningState - /** Table 인스턴스를 상위 컴포넌트에 전달하는 콜백 */ - onTableReady?: (table: Table<TData>) => void + initialColumnPinning?: ColumnPinningState // 추가 + } export function ClientDataTable<TData, TValue>({ @@ -64,8 +63,7 @@ export function ClientDataTable<TData, TValue>({ maxHeight, onSelectedRowsChange, clearSelection, - initialColumnPinning, - onTableReady + initialColumnPinning }: DataTableProps<TData, TValue>) { // (1) React Table 상태 @@ -120,13 +118,6 @@ export function ClientDataTable<TData, TValue>({ useAutoSizeColumns(table, autoSizeColumns) - // 🆕 Table 인스턴스를 상위 컴포넌트에 전달 - React.useEffect(() => { - if (onTableReady) { - onTableReady(table) - } - }, [table, onTableReady]) - React.useEffect(() => { if (!onSelectedRowsChange) return const selectedRows = table @@ -173,7 +164,6 @@ export function ClientDataTable<TData, TValue>({ }), } } - // 🎯 테이블 총 너비 계산 const getTableWidth = React.useCallback(() => { const totalSize = table.getCenterTotalSize() + table.getLeftTotalSize() + table.getRightTotalSize() @@ -216,172 +206,174 @@ export function ClientDataTable<TData, TValue>({ {children} </ClientDataTableAdvancedToolbar> - <div - className="max-w-[100vw] overflow-auto" - style={{ maxHeight: maxHeight || '34rem' }} - onScroll={handleScroll} // 🎯 스크롤 이벤트 핸들러 추가 - > - <UiTable + + <div + className="max-w-[100vw] overflow-auto" + style={{ maxHeight: maxHeight || '34rem' }} + onScroll={handleScroll} // 🎯 스크롤 이벤트 핸들러 추가 + > + <UiTable className={cn( "[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10", !hasNestedHeader && "table-fixed" // nested header가 없으면 table-fixed 적용 )} style={{ minWidth: hasNestedHeader ? getTableWidth() : undefined }}> - {/* nested header가 있으면 table-fixed 제거, 없으면 적용 */} - <TableHeader> - {table.getHeaderGroups().map((headerGroup) => ( - <TableRow key={headerGroup.id} className={compactStyles.headerRow}> - {headerGroup.headers.map((header) => { - // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 - if (header.column.getIsGrouped()) { - return null - } + {/* nested header가 있으면 table-fixed 제거, 없으면 적용 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id} className={compactStyles.headerRow}> + {headerGroup.headers.map((header) => { + // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 + if (header.column.getIsGrouped()) { + return null + } - return ( - <TableHead - key={header.id} - colSpan={header.colSpan} - data-column-id={header.column.id} - className={compactStyles.header} - style={{ - ...getPinnedStyle(header.column, true), // 🎯 헤더임을 명시 - // 부모 그룹 헤더는 colSpan으로 너비가 결정되므로 width 설정하지 않음 - // 자식 헤더만 개별 width 설정 - ...(!('columns' in header.column.columnDef) && { width: header.getSize() }), - }} - > - <div style={{ position: "relative" }}> - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() + return ( + <TableHead + key={header.id} + colSpan={header.colSpan} + data-column-id={header.column.id} + className={compactStyles.header} + style={{ + ...getPinnedStyle(header.column, true), // 🎯 헤더임을 명시 + // 부모 그룹 헤더는 colSpan으로 너비가 결정되므로 width 설정하지 않음 + // 자식 헤더만 개별 width 설정 + ...(!('columns' in header.column.columnDef) && { width: header.getSize() }), + }} + > + <div style={{ position: "relative" }}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */} + {header.column.getCanResize() && !('columns' in header.column.columnDef) && ( + <DataTableResizer header={header} /> )} - - {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */} - {header.column.getCanResize() && !('columns' in header.column.columnDef) && ( - <DataTableResizer header={header} /> - )} - </div> - </TableHead> - ) - })} - </TableRow> - ))} - </TableHeader> - <TableBody> - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => { - // --------------------------------------------------- - // 1) "그룹핑 헤더" Row인지 확인 - // --------------------------------------------------- - if (row.getIsGrouped()) { - // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음 - const groupingColumnId = row.groupingColumnId ?? "" - const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체 + </div> + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => { + // --------------------------------------------------- + // 1) "그룹핑 헤더" Row인지 확인 + // --------------------------------------------------- + if (row.getIsGrouped()) { + // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음 + const groupingColumnId = row.groupingColumnId ?? "" + const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체 - // 컬럼 라벨 가져오기 - let columnLabel = groupingColumnId - if (groupingColumn) { - const headerDef = groupingColumn.columnDef.meta?.excelHeader - if (typeof headerDef === "string") { - columnLabel = headerDef + // 컬럼 라벨 가져오기 + let columnLabel = groupingColumnId + if (groupingColumn) { + const headerDef = groupingColumn.columnDef.meta?.excelHeader + if (typeof headerDef === "string") { + columnLabel = headerDef + } } + + return ( + <TableRow + key={row.id} + className={compactStyles.groupRow} + data-state={row.getIsExpanded() && "expanded"} + > + {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} + <TableCell + colSpan={table.getVisibleFlatColumns().length} + className={compact ? "py-1 px-2" : ""} + > + {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} + {row.getCanExpand() && ( + <button + onClick={row.getToggleExpandedHandler()} + className="inline-flex items-center justify-center mr-2 w-5 h-5" + style={{ + // row.depth: 0이면 top-level, 1이면 그 하위 등 + marginLeft: `${row.depth * 1.5}rem`, + }} + > + {row.getIsExpanded() ? ( + <ChevronUp size={compact ? 14 : 16} /> + ) : ( + <ChevronRight size={compact ? 14 : 16} /> + )} + </button> + )} + + {/* Group Label + 값 */} + <span className="font-semibold"> + {columnLabel}: {row.getValue(groupingColumnId)} + </span> + <span className="ml-2 text-xs text-muted-foreground"> + ({row.subRows.length} rows) + </span> + </TableCell> + </TableRow> + ) } + // --------------------------------------------------- + // 2) 일반 Row + // → "그룹핑된 컬럼"은 숨긴다 + // --------------------------------------------------- return ( <TableRow key={row.id} - className={compactStyles.groupRow} - data-state={row.getIsExpanded() && "expanded"} + className={compactStyles.row} + data-state={row.getIsSelected() && "selected"} > - {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} - <TableCell - colSpan={table.getVisibleFlatColumns().length} - className={compact ? "py-1 px-2" : ""} - > - {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} - {row.getCanExpand() && ( - <button - onClick={row.getToggleExpandedHandler()} - className="inline-flex items-center justify-center mr-2 w-5 h-5" + {row.getVisibleCells().map((cell) => { + // 이 셀의 컬럼이 grouped라면 숨긴다 + if (cell.column.getIsGrouped()) { + return null + } + + return ( + <TableCell + key={cell.id} + data-column-id={cell.column.id} + className={compactStyles.cell} style={{ - // row.depth: 0이면 top-level, 1이면 그 하위 등 - marginLeft: `${row.depth * 1.5}rem`, + ...getPinnedStyle(cell.column, false), // 🎯 바디 셀임을 명시 + width: cell.column.getSize() // 🎯 width 별도 설정 }} > - {row.getIsExpanded() ? ( - <ChevronUp size={compact ? 14 : 16} /> - ) : ( - <ChevronRight size={compact ? 14 : 16} /> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() )} - </button> - )} - - {/* Group Label + 값 */} - <span className="font-semibold"> - {columnLabel}: {row.getValue(groupingColumnId)} - </span> - <span className="ml-2 text-xs text-muted-foreground"> - ({row.subRows.length} rows) - </span> - </TableCell> + </TableCell> + ) + })} </TableRow> ) - } - + }) + ) : ( // --------------------------------------------------- - // 2) 일반 Row - // → "그룹핑된 컬럼"은 숨긴다 + // 3) 데이터가 없을 때 // --------------------------------------------------- - return ( - <TableRow - key={row.id} - className={compactStyles.row} - data-state={row.getIsSelected() && "selected"} + <TableRow> + <TableCell + colSpan={table.getAllColumns().length} + className={compactStyles.emptyRow + " text-center"} > - {row.getVisibleCells().map((cell) => { - // 이 셀의 컬럼이 grouped라면 숨긴다 - if (cell.column.getIsGrouped()) { - return null - } + No results. + </TableCell> + </TableRow> + )} + </TableBody> + </UiTable> + </div> - return ( - <TableCell - key={cell.id} - data-column-id={cell.column.id} - className={compactStyles.cell} - style={{ - ...getPinnedStyle(cell.column, false), // 🎯 바디 셀임을 명시 - width: cell.column.getSize() // 🎯 width 별도 설정 - }} - > - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - </TableCell> - ) - })} - </TableRow> - ) - }) - ) : ( - // --------------------------------------------------- - // 3) 데이터가 없을 때 - // --------------------------------------------------- - <TableRow> - <TableCell - colSpan={table.getAllColumns().length} - className={compactStyles.emptyRow + " text-center"} - > - No results. - </TableCell> - </TableRow> - )} - </TableBody> - </UiTable> - </div> <ClientDataTablePagination table={table} /> </div> diff --git a/components/form-data-plant/delete-form-data-dialog.tsx b/components/form-data-plant/delete-form-data-dialog.tsx index 2406407e..6ac8f67c 100644 --- a/components/form-data-plant/delete-form-data-dialog.tsx +++ b/components/form-data-plant/delete-form-data-dialog.tsx @@ -40,8 +40,7 @@ interface DeleteFormDataDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { formData: GenericData[] formCode: string - projectCode: string - packageCode: string + contractItemId: number projectId?: number showTrigger?: boolean onSuccess?: () => void @@ -51,8 +50,7 @@ interface DeleteFormDataDialogProps export function DeleteFormDataDialog({ formData, formCode, - projectCode, - packageCode, + contractItemId, projectId, showTrigger = true, onSuccess, @@ -79,8 +77,7 @@ export function DeleteFormDataDialog({ const result = await deleteFormDataByTags({ formCode, - projectCode, - packageCode, + contractItemId, tagIdxs, projectId, }) diff --git a/components/form-data-plant/form-data-report-batch-dialog.tsx b/components/form-data-plant/form-data-report-batch-dialog.tsx index ba41a3c2..24b5827b 100644 --- a/components/form-data-plant/form-data-report-batch-dialog.tsx +++ b/components/form-data-plant/form-data-report-batch-dialog.tsx @@ -71,8 +71,7 @@ interface FormDataReportBatchDialogProps { setOpen: Dispatch<SetStateAction<boolean>>; columnsJSON: DataTableColumnJSON[]; reportData: ReportData[]; - projectCode: string; - packageCode: string; + packageId: number; formId: number; formCode: string; } @@ -82,8 +81,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ setOpen, columnsJSON, reportData, - projectCode, - packageCode, + packageId, formId, formCode, }) => { @@ -102,8 +100,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null); useEffect(() => { - updateReportTempList(projectCode, packageCode, formId, setTempList); - }, [projectCode, packageCode, formId]); + updateReportTempList(packageId, formId, setTempList); + }, [packageId, formId]); const onClose = () => { if (isUploading) { @@ -363,8 +361,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ <PublishDialog open={publishDialogOpen} onOpenChange={setPublishDialogOpen} - projectCode={projectCode} - packageCode={packageCode} + packageId={packageId} formCode={formCode} fileBlob={generatedFileBlob || undefined} /> @@ -412,19 +409,17 @@ const UploadFileItem: FC<UploadFileItemProps> = ({ }; type UpdateReportTempList = ( - projectCode: string, - packageCode: string, + packageId: number, formId: number, setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>> ) => void; const updateReportTempList: UpdateReportTempList = async ( - projectCode, - packageCode, + packageId, formId, setTempList ) => { - const tempList = await getReportTempList(projectCode,packageCode, formId); + const tempList = await getReportTempList(packageId, formId); setTempList( tempList.map((c) => { diff --git a/components/form-data-plant/form-data-report-dialog.tsx b/components/form-data-plant/form-data-report-dialog.tsx index 2413fc28..9177ab36 100644 --- a/components/form-data-plant/form-data-report-dialog.tsx +++ b/components/form-data-plant/form-data-report-dialog.tsx @@ -49,8 +49,7 @@ interface FormDataReportDialogProps { columnsJSON: DataTableColumnJSON[]; reportData: ReportData[]; setReportData: Dispatch<SetStateAction<ReportData[]>>; - projectCode: string; - packageCode: string; + packageId: number; formId: number; formCode: string; } @@ -59,8 +58,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({ columnsJSON, reportData, setReportData, - projectCode, - packageCode, + packageId, formId, formCode, }) => { @@ -78,8 +76,8 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({ const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null); useEffect(() => { - updateReportTempList(projectCode, packageCode, formId, setTempList); - }, [projectCode,packageCode, formId]); + updateReportTempList(packageId, formId, setTempList); + }, [packageId, formId]); const onClose = async (value: boolean) => { if (fileLoading) { @@ -199,8 +197,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({ <PublishDialog open={publishDialogOpen} onOpenChange={setPublishDialogOpen} - projectCode={projectCode} - packageCode={packageCode} + packageId={packageId} formCode={formCode} fileBlob={generatedFileBlob || undefined} /> @@ -397,19 +394,17 @@ const importReportData: ImportReportData = async ( }; type UpdateReportTempList = ( - projectCode: string, - packageCode: string, + packageId: number, formId: number, setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>> ) => void; const updateReportTempList: UpdateReportTempList = async ( - projectCode, - packageCode, + packageId, formId, setTempList ) => { - const tempList = await getReportTempList(projectCode,packageCode, formId); + const tempList = await getReportTempList(packageId, formId); setTempList( tempList.map((c) => { diff --git a/components/form-data-plant/form-data-report-temp-upload-dialog.tsx b/components/form-data-plant/form-data-report-temp-upload-dialog.tsx index 66915198..59ea6ade 100644 --- a/components/form-data-plant/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data-plant/form-data-report-temp-upload-dialog.tsx @@ -23,8 +23,7 @@ interface FormDataReportTempUploadDialogProps { columnsJSON: DataTableColumnJSON[]; open: boolean; setOpen: Dispatch<SetStateAction<boolean>>; - projectCode: string; - packageCode: string; + packageId: number; formCode: string; formId: number; uploaderType: string; @@ -36,8 +35,7 @@ export const FormDataReportTempUploadDialog: FC< columnsJSON, open, setOpen, - projectCode, - packageCode, + packageId, formId, formCode, uploaderType, @@ -85,16 +83,14 @@ export const FormDataReportTempUploadDialog: FC< </div> <TabsContent value="upload"> <FormDataReportTempUploadTab - projectCode={projectCode} - packageCode={packageCode} + packageId={packageId} formId={formId} uploaderType={uploaderType} /> </TabsContent> <TabsContent value="uploaded"> <FormDataReportTempUploadedListTab - projectCode={projectCode} - packageCode={packageCode} + packageId={packageId} formId={formId} /> </TabsContent> diff --git a/components/form-data-plant/form-data-report-temp-upload-tab.tsx b/components/form-data-plant/form-data-report-temp-upload-tab.tsx index 41466f90..81186ba4 100644 --- a/components/form-data-plant/form-data-report-temp-upload-tab.tsx +++ b/components/form-data-plant/form-data-report-temp-upload-tab.tsx @@ -36,15 +36,14 @@ import { uploadReportTemp } from "@/lib/forms-plant/services"; const MAX_FILE_SIZE = 3000000; interface FormDataReportTempUploadTabProps { - projectCode: string; - packageCode: string; + packageId: number; formId: number; uploaderType: string; } export const FormDataReportTempUploadTab: FC< FormDataReportTempUploadTabProps -> = ({ projectCode,packageCode, formId, uploaderType }) => { +> = ({ packageId, formId, uploaderType }) => { const { toast } = useToast(); const params = useParams(); const lng = (params?.lng as string) || "ko"; @@ -95,7 +94,7 @@ export const FormDataReportTempUploadTab: FC< formData.append("customFileName", file.name); formData.append("uploaderType", uploaderType); - await uploadReportTemp(projectCode, packageCode, formId, formData); + await uploadReportTemp(packageId, formId, formData); successCount++; setUploadProgress(Math.round((successCount / totalFiles) * 100)); diff --git a/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx b/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx index 1b6cefaf..4cfbad69 100644 --- a/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx +++ b/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx @@ -39,14 +39,13 @@ import { getReportTempList, deleteReportTempFile } from "@/lib/forms-plant/servi import { VendorDataReportTemps } from "@/db/schema/vendorData"; interface FormDataReportTempUploadedListTabProps { - projectCode: string; - packageCode: string; + packageId: number; formId: number; } export const FormDataReportTempUploadedListTab: FC< FormDataReportTempUploadedListTabProps -> = ({ projectCode,packageCode , formId }) => { +> = ({ packageId, formId }) => { const params = useParams(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "engineering"); @@ -58,12 +57,12 @@ export const FormDataReportTempUploadedListTab: FC< useEffect(() => { const getTempFiles = async () => { - await updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp); + await updateReportTempList(packageId, formId, setPrevReportTemp); setIsLoading(false); }; getTempFiles(); - }, [projectCode,packageCode, formId]); + }, [packageId, formId]); return ( <div> @@ -71,7 +70,7 @@ export const FormDataReportTempUploadedListTab: FC< <UploadedTempFiles prevReportTemp={prevReportTemp} updateReportTempList={() => - updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp) + updateReportTempList(packageId, formId, setPrevReportTemp) } isLoading={isLoading} t={t} @@ -81,19 +80,17 @@ export const FormDataReportTempUploadedListTab: FC< }; type UpdateReportTempList = ( - projectCode: string, - packageCode: string, + packageId: number, formId: number, setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>> ) => Promise<void>; const updateReportTempList: UpdateReportTempList = async ( - projectCode, - packageCode, + packageId, formId, setPrevReportTemp ) => { - const tempList = await getReportTempList(projectCode, packageCode, formId); + const tempList = await getReportTempList(packageId, formId); setPrevReportTemp(tempList); }; diff --git a/components/form-data-plant/form-data-table.tsx b/components/form-data-plant/form-data-table.tsx index c6c79a69..30c176bd 100644 --- a/components/form-data-plant/form-data-table.tsx +++ b/components/form-data-plant/form-data-table.tsx @@ -76,8 +76,7 @@ interface GenericData { export interface DynamicTableProps { dataJSON: GenericData[]; columnsJSON: DataTableColumnJSON[]; - projectCode: string; - packageCode: string; + contractItemId: number; formCode: string; formId: number; projectId: number; @@ -90,8 +89,7 @@ export interface DynamicTableProps { export default function DynamicTable({ dataJSON, columnsJSON, - projectCode, - packageCode, + contractItemId, formCode, formId, projectId, @@ -158,8 +156,7 @@ export default function DynamicTable({ // 서버 액션 호출 const result = await excludeFormDataByTags({ formCode, - projectCode, - packageCode, + contractItemId, tagNumbers, }); @@ -291,7 +288,7 @@ export default function DynamicTable({ try { setIsLoadingStats(true); // getFormStatusByVendor 서버 액션 직접 호출 - const data = await getFormStatusByVendor(projectId, projectCode, packageCode,formCode); + const data = await getFormStatusByVendor(projectId, contractItemId, formCode); if (data && data.length > 0) { setFormStats(data[0]); @@ -342,7 +339,9 @@ export default function DynamicTable({ // SEDP compare dialog state const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false); - const projectType = "plant"; + const [projectCode, setProjectCode] = React.useState<string>(''); + const [projectType, setProjectType] = React.useState<string>('plant'); + const [packageCode, setPackageCode] = React.useState<string>(''); // 새로 추가된 Template 다이얼로그 상태 const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false); @@ -375,13 +374,43 @@ const [isLoadingRegisters, setIsLoadingRegisters] = React.useState(false); React.useEffect(() => { const getTempCount = async () => { - const tempList = await getReportTempList(projectCode, packageCode, formId); + const tempList = await getReportTempList(contractItemId, formId); setTempCount(tempList.length); }; getTempCount(); - }, [projectCode,packageCode, formId, tempUpDialog]); + }, [contractItemId, formId, tempUpDialog]); + React.useEffect(() => { + const getPackageCode = async () => { + try { + const packageCode = await getPackageCodeById(contractItemId); + setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값 + } catch (error) { + console.error('패키지 조회 실패:', error); + setPackageCode(''); + } + }; + + getPackageCode(); + }, [contractItemId]) + // Get project code when component mounts + React.useEffect(() => { + const getProjectCode = async () => { + try { + const project = await getProjectById(projectId); + setProjectCode(project.code); + setProjectType(project.type); + } catch (error) { + console.error("Error fetching project code:", error); + toast.error("Failed to fetch project code"); + } + }; + + if (projectId) { + getProjectCode(); + } + }, [projectId]); // 선택된 행들의 실제 데이터 가져오기 const getSelectedRowsData = React.useCallback(() => { @@ -500,7 +529,7 @@ React.useEffect(() => { async function handleSyncTags() { try { setIsSyncingTags(true); - const result = await syncMissingTags(projectCode,packageCode, formCode); + const result = await syncMissingTags(contractItemId, formCode); // Prepare the toast messages based on what changed const changes = []; @@ -533,9 +562,9 @@ React.useEffect(() => { setIsLoadingTags(true); // API 엔드포인트 호출 - 작업 시작만 요청 - const response = await fetch('/api/cron/form-tags-plant/start', { + const response = await fetch('/api/cron/form-tags/start', { method: 'POST', - body: JSON.stringify({ projectCode, formCode, packageCode }) + body: JSON.stringify({ projectCode, formCode, contractItemId }) }); if (!response.ok) { @@ -574,7 +603,7 @@ React.useEffect(() => { // 5초마다 상태 확인 pollingRef.current = setInterval(async () => { try { - const response = await fetch(`/api/cron/form-tags-plant/status?id=${id}`); + const response = await fetch(`/api/cron/form-tags/status?id=${id}`); if (!response.ok) { throw new Error('Failed to get tag import status'); @@ -637,8 +666,7 @@ React.useEffect(() => { tableData, columnsJSON, formCode, - projectCode, - packageCode, + contractItemId, editableFieldsMap, // 추가: 편집 가능 필드 정보 전달 onPendingChange: setIsImporting, // Let importExcelData handle loading state onDataUpdate: (newData) => { @@ -719,8 +747,7 @@ React.useEffect(() => { const sedpResult = await sendFormDataToSEDP( formCode, // Send formCode instead of formName projectId, // Project ID - projectCode, - packageCode, + contractItemId, tableData.filter(v=>v.status !== 'excluded'), // Table data columnsJSON // Column definitions ); @@ -1199,8 +1226,7 @@ React.useEffect(() => { columns={columnsJSON} rowData={rowAction?.row.original ?? null} formCode={formCode} - projectCode={projectCode} - packageCode={packageCode} + contractItemId={contractItemId} editableFieldsMap={editableFieldsMap} onUpdateSuccess={(updatedValues) => { // Update the specific row in tableData when a single row is updated @@ -1218,8 +1244,7 @@ React.useEffect(() => { <DeleteFormDataDialog formData={deleteTarget} formCode={formCode} - projectCode={projectCode} - packageCode={packageCode} + contractItemId={contractItemId} projectId={projectId} open={deleteDialogOpen} onOpenChange={(open) => { @@ -1232,6 +1257,16 @@ React.useEffect(() => { showTrigger={false} /> + {/* Dialog for adding tags */} + {/* <AddFormTagDialog + projectId={projectId} + formCode={formCode} + formName={`Form ${formCode}`} + contractItemId={contractItemId} + packageCode={packageCode} + open={addTagDialogOpen} + onOpenChange={setAddTagDialogOpen} + /> */} {/* 새로 추가된 Template 다이얼로그 */} <TemplateViewDialog @@ -1241,8 +1276,7 @@ React.useEffect(() => { selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용 tableData={tableData} // SPR_LST_SETUP용 - 새로 추가 formCode={formCode} - projectCode={projectCode} - packageCode={packageCode} + contractItemId={contractItemId} editableFieldsMap={editableFieldsMap} columnsJSON={columnsJSON} onUpdateSuccess={(updatedValues) => { @@ -1310,8 +1344,7 @@ React.useEffect(() => { columnsJSON={columnsJSON} open={tempUpDialog} setOpen={setTempUpDialog} - projectCode={projectCode} - packageCode={packageCode} + packageId={contractItemId} formCode={formCode} formId={formId} uploaderType="vendor" @@ -1323,8 +1356,7 @@ React.useEffect(() => { columnsJSON={columnsJSON} reportData={reportData} setReportData={setReportData} - projectCode={projectCode} - packageCode={packageCode} + packageId={contractItemId} formCode={formCode} formId={formId} /> @@ -1336,8 +1368,7 @@ React.useEffect(() => { setOpen={setBatchDownDialog} columnsJSON={columnsJSON} reportData={selectedRowCount > 0 ? getSelectedRowsData() : tableData} - projectCode={projectCode} - packageCode={packageCode} + packageId={contractItemId} formCode={formCode} formId={formId} /> diff --git a/components/form-data-plant/import-excel-form.tsx b/components/form-data-plant/import-excel-form.tsx index 8ac70c59..ffc6f2f9 100644 --- a/components/form-data-plant/import-excel-form.tsx +++ b/components/form-data-plant/import-excel-form.tsx @@ -23,8 +23,7 @@ export interface ImportExcelOptions { tableData: GenericData[]; columnsJSON: DataTableColumnJSON[]; formCode?: string; - projectCode: string; - packageCode: string; + contractItemId?: number; editableFieldsMap?: Map<string, string[]>; // 새로 추가 onPendingChange?: (isPending: boolean) => void; onDataUpdate?: (updater: ((prev: GenericData[]) => GenericData[]) | GenericData[]) => void; @@ -219,8 +218,7 @@ export async function importExcelData({ tableData, columnsJSON, formCode, - projectCode, - packageCode, + contractItemId, editableFieldsMap = new Map(), // 새로 추가 onPendingChange, onDataUpdate @@ -529,14 +527,14 @@ export async function importExcelData({ } }); + // If formCode and contractItemId are provided, save directly to DB // importExcelData 함수에서 DB 저장 부분 - if (formCode && projectCode && packageCode) { + if (formCode && contractItemId) { try { // 배치 업데이트 함수 호출 const result = await updateFormDataBatchInDB( formCode, - projectCode, - packageCode, + contractItemId, importedData // 모든 imported rows를 한번에 전달 ); @@ -635,6 +633,7 @@ export async function importExcelData({ } } else { + // formCode나 contractItemId가 없는 경우 - 로컬 업데이트만 if (onDataUpdate) { onDataUpdate(() => mergedData); } diff --git a/components/form-data-plant/publish-dialog.tsx b/components/form-data-plant/publish-dialog.tsx index f63c2db8..a3a2ef0b 100644 --- a/components/form-data-plant/publish-dialog.tsx +++ b/components/form-data-plant/publish-dialog.tsx @@ -37,21 +37,19 @@ import { Loader2, Check, ChevronsUpDown } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { - createSubmissionAction, // 새로운 액션 이름 - fetchDocumentsByProjectAndPackage, // 업데이트된 액션 - fetchStagesByDocumentIdPlant, - fetchSubmissionsByStageParams, // revisions 대신 submissions + createRevisionAction, + fetchDocumentsByPackageId, + fetchStagesByDocumentId, + fetchRevisionsByStageParams, + Document, + IssueStage, + Revision } from "@/lib/vendor-document/service"; -import type { - StageDocument, - StageIssueStage, -} from "@/db/schema/vendorDocu"; interface PublishDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - projectCode: string; - packageCode: string; + packageId: number; formCode: string; fileBlob?: Blob; } @@ -59,8 +57,7 @@ interface PublishDialogProps { export const PublishDialog: React.FC<PublishDialogProps> = ({ open, onOpenChange, - projectCode, - packageCode, + packageId, formCode, fileBlob, }) => { @@ -68,10 +65,9 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ const { data: session } = useSession(); // State for form data - const [documents, setDocuments] = useState<StageDocument[]>([]); - const [stages, setStages] = useState<StageIssueStage[]>([]); - const [latestRevisionCode, setLatestRevisionCode] = useState<string>(""); - const [latestRevisionNumber, setLatestRevisionNumber] = useState<number>(0); + const [documents, setDocuments] = useState<Document[]>([]); + const [stages, setStages] = useState<IssueStage[]>([]); + const [latestRevision, setLatestRevision] = useState<string>(""); // State for document search const [openDocumentCombobox, setOpenDocumentCombobox] = useState(false); @@ -81,10 +77,9 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ const [selectedDocId, setSelectedDocId] = useState<string>(""); const [selectedDocumentDisplay, setSelectedDocumentDisplay] = useState<string>(""); const [selectedStage, setSelectedStage] = useState<string>(""); - const [revisionCodeInput, setRevisionCodeInput] = useState<string>(""); - const [submitterName, setSubmitterName] = useState<string>(""); - const [submissionTitle, setSubmissionTitle] = useState<string>(""); - const [submissionDescription, setSubmissionDescription] = useState<string>(""); + const [revisionInput, setRevisionInput] = useState<string>(""); + const [uploaderName, setUploaderName] = useState<string>(""); + const [comment, setComment] = useState<string>(""); const [customFileName, setCustomFileName] = useState<string>(`${formCode}_document.docx`); // Loading states @@ -99,10 +94,10 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ ) : documents; - // Set submitter name from session when dialog opens + // Set uploader name from session when dialog opens useEffect(() => { if (open && session?.user?.name) { - setSubmitterName(session.user.name); + setUploaderName(session.user.name); } }, [open, session]); @@ -112,26 +107,24 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ setSelectedDocId(""); setSelectedDocumentDisplay(""); setSelectedStage(""); - setRevisionCodeInput(""); - setSubmissionTitle(""); - setSubmissionDescription(""); - // Only set submitterName if not already set from session - if (!session?.user?.name) setSubmitterName(""); - setLatestRevisionCode(""); - setLatestRevisionNumber(0); + setRevisionInput(""); + // Only set uploaderName if not already set from session + if (!session?.user?.name) setUploaderName(""); + setComment(""); + setLatestRevision(""); setCustomFileName(`${formCode}_document.docx`); setDocumentSearchValue(""); } }, [open, formCode, session]); - // Fetch documents based on projectCode and packageCode + // Fetch documents based on packageId useEffect(() => { async function loadDocuments() { - if (projectCode && packageCode && open) { + if (packageId && open) { setIsLoading(true); try { - const docs = await fetchDocumentsByProjectAndPackage(projectCode, packageCode); + const docs = await fetchDocumentsByPackageId(packageId); setDocuments(docs); } catch (error) { console.error("Error fetching documents:", error); @@ -143,7 +136,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ } loadDocuments(); - }, [projectCode, packageCode, open]); + }, [packageId, open]); // Fetch stages when document is selected useEffect(() => { @@ -153,12 +146,11 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ // Reset dependent fields setSelectedStage(""); - setRevisionCodeInput(""); - setLatestRevisionCode(""); - setLatestRevisionNumber(0); + setRevisionInput(""); + setLatestRevision(""); try { - const stagesList = await fetchStagesByDocumentIdPlant(parseInt(selectedDocId, 10)); + const stagesList = await fetchStagesByDocumentId(parseInt(selectedDocId, 10)); setStages(stagesList); } catch (error) { console.error("Error fetching stages:", error); @@ -174,78 +166,65 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ loadStages(); }, [selectedDocId]); - // Fetch latest submission (revision) when stage is selected + // Fetch latest revision when stage is selected (for reference) useEffect(() => { - async function loadLatestSubmission() { + async function loadLatestRevision() { if (selectedDocId && selectedStage) { setIsLoading(true); try { - const submissionsList = await fetchSubmissionsByStageParams( + const revsList = await fetchRevisionsByStageParams( parseInt(selectedDocId, 10), selectedStage ); - // Find the latest submission (assuming sorted by revision number) - if (submissionsList.length > 0) { - // Sort submissions by revision number descending - const sortedSubmissions = [...submissionsList].sort((a, b) => - b.revisionNumber - a.revisionNumber - ); + // Find the latest revision (assuming revisions are sorted by revision number) + if (revsList.length > 0) { + // Sort revisions if needed + const sortedRevisions = [...revsList].sort((a, b) => { + return b.revision.localeCompare(a.revision, undefined, { numeric: true }); + }); - const latestSubmission = sortedSubmissions[0]; - setLatestRevisionCode(latestSubmission.revisionCode); - setLatestRevisionNumber(latestSubmission.revisionNumber); + setLatestRevision(sortedRevisions[0].revision); - // Auto-increment revision code - if (latestSubmission.revisionCode.match(/^\d+$/)) { + // Pre-fill the revision input with an incremented value if possible + if (sortedRevisions[0].revision.match(/^\d+$/)) { // If it's a number, increment it - const nextRevision = String(parseInt(latestSubmission.revisionCode, 10) + 1); - setRevisionCodeInput(nextRevision); - } else if (latestSubmission.revisionCode.match(/^[A-Za-z]$/)) { + const nextRevision = String(parseInt(sortedRevisions[0].revision, 10) + 1); + setRevisionInput(nextRevision); + } else if (sortedRevisions[0].revision.match(/^[A-Za-z]$/)) { // If it's a single letter, get the next letter - const currentChar = latestSubmission.revisionCode.charCodeAt(0); + const currentChar = sortedRevisions[0].revision.charCodeAt(0); const nextChar = String.fromCharCode(currentChar + 1); - setRevisionCodeInput(nextChar); - } else if (latestSubmission.revisionCode.toLowerCase().startsWith("rev")) { - // Handle "Rev0", "Rev1" format - const numMatch = latestSubmission.revisionCode.match(/\d+$/); - if (numMatch) { - const nextNum = parseInt(numMatch[0], 10) + 1; - setRevisionCodeInput(`Rev${nextNum}`); - } else { - setRevisionCodeInput(""); - } + setRevisionInput(nextChar); } else { // For other formats, just show the latest as reference - setRevisionCodeInput(""); + setRevisionInput(""); } } else { - // If no submissions exist, set default values - setLatestRevisionCode(""); - setLatestRevisionNumber(0); - setRevisionCodeInput("Rev0"); // Start with Rev0 + // If no revisions exist, set default values + setLatestRevision(""); + setRevisionInput("0"); } } catch (error) { - console.error("Error fetching submissions:", error); - toast.error("Failed to load submission information"); + console.error("Error fetching revisions:", error); + toast.error("Failed to load revision information"); } finally { setIsLoading(false); } } else { - setLatestRevisionCode(""); - setLatestRevisionNumber(0); - setRevisionCodeInput(""); + setLatestRevision(""); + setRevisionInput(""); } } - loadLatestSubmission(); + loadLatestRevision(); }, [selectedDocId, selectedStage]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!selectedDocId || !selectedStage || !revisionCodeInput || !fileBlob) { + if (!selectedDocId || !selectedStage || !revisionInput || !fileBlob) { toast.error("Please fill in all required fields"); return; } @@ -256,30 +235,17 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ // Create FormData const formData = new FormData(); formData.append("documentId", selectedDocId); - formData.append("stageName", selectedStage); - formData.append("revisionCode", revisionCodeInput); + formData.append("stage", selectedStage); + formData.append("revision", revisionInput); formData.append("customFileName", customFileName); + formData.append("uploaderType", "vendor"); // Default value - if (submitterName) { - formData.append("submittedBy", submitterName); + if (uploaderName) { + formData.append("uploaderName", uploaderName); } - if (session?.user?.email) { - formData.append("submittedByEmail", session.user.email); - } - - if (submissionTitle) { - formData.append("submissionTitle", submissionTitle); - } - - if (submissionDescription) { - formData.append("submissionDescription", submissionDescription); - } - - // Get vendor info from selected document - const selectedDoc = documents.find(doc => doc.id === parseInt(selectedDocId, 10)); - if (selectedDoc) { - formData.append("vendorId", String(selectedDoc.vendorId)); + if (comment) { + formData.append("comment", comment); } // Append file as attachment @@ -290,14 +256,12 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ formData.append("attachment", file); } - // Call server action - const result = await createSubmissionAction(formData); + // Call server action directly + const result = await createRevisionAction(formData); - if (result.success) { + if (result) { toast.success("Document published successfully!"); onOpenChange(false); - } else { - toast.error(result.error || "Failed to publish document"); } } catch (error) { console.error("Error publishing document:", error); @@ -337,6 +301,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ className="w-full justify-between" disabled={isLoading || documents.length === 0} > + {/* Add text-overflow handling for selected document display */} <span className="truncate"> {selectedDocumentDisplay ? selectedDocumentDisplay @@ -373,6 +338,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ : "opacity-0" )} /> + {/* Add text-overflow handling for document items */} <span className="truncate">{doc.docNumber} - {doc.title}</span> </CommandItem> ))} @@ -400,6 +366,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ <SelectContent> {stages.map((stage) => ( <SelectItem key={stage.id} value={stage.stageName}> + {/* Add text-overflow handling for stage names */} <span className="truncate">{stage.stageName}</span> </SelectItem> ))} @@ -408,42 +375,28 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ </div> </div> - {/* Revision Code Input */} + {/* Revision Input */} <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="revisionCode" className="text-right"> + <Label htmlFor="revision" className="text-right"> Revision </Label> <div className="col-span-3"> <Input - id="revisionCode" - value={revisionCodeInput} - onChange={(e) => setRevisionCodeInput(e.target.value)} - placeholder="Enter revision code (e.g., Rev0, A, 1)" + id="revision" + value={revisionInput} + onChange={(e) => setRevisionInput(e.target.value)} + placeholder="Enter revision" disabled={isLoading || !selectedStage} /> - {latestRevisionCode && ( + {latestRevision && ( <p className="text-xs text-muted-foreground mt-1"> - Latest revision: {latestRevisionCode} (#{latestRevisionNumber}) + Latest revision: {latestRevision} </p> )} </div> </div> <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="submissionTitle" className="text-right"> - Title - </Label> - <div className="col-span-3"> - <Input - id="submissionTitle" - value={submissionTitle} - onChange={(e) => setSubmissionTitle(e.target.value)} - placeholder="Optional submission title" - /> - </div> - </div> - - <div className="grid grid-cols-4 items-center gap-4"> <Label htmlFor="fileName" className="text-right"> File Name </Label> @@ -458,15 +411,16 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ </div> <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="submitterName" className="text-right"> - Submitter + <Label htmlFor="uploaderName" className="text-right"> + Uploader </Label> <div className="col-span-3"> <Input - id="submitterName" - value={submitterName} - onChange={(e) => setSubmitterName(e.target.value)} + id="uploaderName" + value={uploaderName} + onChange={(e) => setUploaderName(e.target.value)} placeholder="Your name" + // Disable input but show a filled style className={session?.user?.name ? "opacity-70" : ""} readOnly={!!session?.user?.name} /> @@ -479,15 +433,15 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ </div> <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="description" className="text-right"> - Description + <Label htmlFor="comment" className="text-right"> + Comment </Label> <div className="col-span-3"> <Textarea - id="description" - value={submissionDescription} - onChange={(e) => setSubmissionDescription(e.target.value)} - placeholder="Optional submission description" + id="comment" + value={comment} + onChange={(e) => setComment(e.target.value)} + placeholder="Optional comment" className="resize-none" /> </div> @@ -497,7 +451,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({ <DialogFooter> <Button type="submit" - disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionCodeInput} + disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionInput} > {isSubmitting ? ( <> diff --git a/components/form-data-plant/spreadJS-dialog.tsx b/components/form-data-plant/spreadJS-dialog.tsx index 9f972676..2eb2c8ba 100644 --- a/components/form-data-plant/spreadJS-dialog.tsx +++ b/components/form-data-plant/spreadJS-dialog.tsx @@ -92,8 +92,7 @@ interface TemplateViewDialogProps { tableData?: GenericData[]; formCode: string; columnsJSON: DataTableColumnJSON[] - projectCode: string; - packageCode: string; + contractItemId: number; editableFieldsMap?: Map<string, string[]>; onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void; } @@ -143,8 +142,7 @@ export function TemplateViewDialog({ selectedRow, tableData = [], formCode, - projectCode, - packageCode, + contractItemId, columnsJSON, editableFieldsMap = new Map(), onUpdateSuccess @@ -1437,8 +1435,7 @@ export function TemplateViewDialog({ const { success, message } = await updateFormDataInDB( formCode, - projectCode, - packageCode, + contractItemId, dataToSave ); @@ -1503,8 +1500,7 @@ export function TemplateViewDialog({ try { const { success, message } = await updateFormDataInDB( formCode, - projectCode, - packageCode, + contractItemId, dataToSave ); @@ -1555,8 +1551,7 @@ export function TemplateViewDialog({ selectedRow, tableData, formCode, - projectCode, - packageCode, + contractItemId, onUpdateSuccess, cellMappings, columnsJSON, diff --git a/components/form-data-plant/update-form-sheet.tsx b/components/form-data-plant/update-form-sheet.tsx index b7f56f7e..bd75d8f3 100644 --- a/components/form-data-plant/update-form-sheet.tsx +++ b/components/form-data-plant/update-form-sheet.tsx @@ -65,8 +65,7 @@ interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef<typeof Shee columns: DataTableColumnJSON[]; rowData: Record<string, any> | null; formCode: string; - projectCode: string; - packageCode: string; + contractItemId: number; editableFieldsMap?: Map<string, string[]>; // 새로 추가 /** 업데이트 성공 시 호출될 콜백 */ onUpdateSuccess?: (updatedValues: Record<string, any>) => void; @@ -78,8 +77,7 @@ export function UpdateTagSheet({ columns, rowData, formCode, - projectCode, - packageCode, + contractItemId, editableFieldsMap = new Map(), onUpdateSuccess, ...props @@ -221,8 +219,7 @@ export function UpdateTagSheet({ const { success, message } = await updateFormDataInDB( formCode, - projectCode, - packageCode, + contractItemId, finalValues, ); diff --git a/components/vendor-data-plant/project-swicher.tsx b/components/vendor-data-plant/project-swicher.tsx index 9b8f9bea..d3123709 100644 --- a/components/vendor-data-plant/project-swicher.tsx +++ b/components/vendor-data-plant/project-swicher.tsx @@ -1,7 +1,6 @@ "use client" import * as React from "react" -import { Check, ChevronsUpDown } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { @@ -17,103 +16,149 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover" +import { Check, ChevronsUpDown, Loader2 } from "lucide-react" -interface PackageData { - packageCode: string - packageName: string | null +interface ContractInfo { + contractId: number + contractName: string } -interface ProjectData { +interface ProjectInfo { projectId: number projectCode: string projectName: string - projectType: string - packages: PackageData[] + contracts: ContractInfo[] } interface ProjectSwitcherProps { isCollapsed: boolean - projects: ProjectData[] - selectedProjectId: number - selectedPackageCode: string | null - onSelectPackage: (projectId: number, packageCode: string) => void + projects: ProjectInfo[] + + // 상위가 관리하는 "현재 선택된 contractId" + selectedContractId: number | null + + // 콜백: 사용자가 "어떤 contract"를 골랐는지 + // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함 + onSelectContract: (projectId: number, contractId: number) => void + + // 로딩 상태 (선택사항) + isLoading?: boolean } export function ProjectSwitcher({ isCollapsed, projects, - selectedProjectId, - selectedPackageCode, - onSelectPackage, + selectedContractId, + onSelectContract, + isLoading = false, }: ProjectSwitcherProps) { - const [open, setOpen] = React.useState(false) + const [popoverOpen, setPopoverOpen] = React.useState(false) + const [searchTerm, setSearchTerm] = React.useState("") - // 현재 선택된 프로젝트와 패키지 정보 - const selectedProject = projects.find(p => p.projectId === selectedProjectId) - const selectedPackage = selectedProject?.packages.find( - pkg => pkg.packageCode === selectedPackageCode - ) + // 현재 선택된 contract 객체 찾기 + const selectedContract = React.useMemo(() => { + if (!selectedContractId) return null + for (const proj of projects) { + const found = proj.contracts.find((c) => c.contractId === selectedContractId) + if (found) { + return { ...found, projectId: proj.projectId, projectName: proj.projectName } + } + } + return null + }, [projects, selectedContractId]) + + // Trigger label => 계약 이름 or placeholder + const triggerLabel = selectedContract?.contractName ?? "Select a contract" + // 검색어에 따른 필터링된 프로젝트/계약 목록 + const filteredProjects = React.useMemo(() => { + if (!searchTerm) return projects + + return projects.map(project => ({ + ...project, + contracts: project.contracts.filter(contract => + contract.contractName.toLowerCase().includes(searchTerm.toLowerCase()) || + project.projectName.toLowerCase().includes(searchTerm.toLowerCase()) + ) + })).filter(project => project.contracts.length > 0) + }, [projects, searchTerm]) - console.log(projects,"projects") + // 계약 선택 핸들러 + function handleSelectContract(projectId: number, contractId: number) { + onSelectContract(projectId, contractId) + setPopoverOpen(false) + setSearchTerm("") // 검색어 초기화 + } - const displayText = selectedPackage - ? `${selectedProject?.projectCode} - ${selectedPackage.packageCode}` - : selectedProject?.projectCode || "Select Package" + // 총 계약 수 계산 (빈 상태 표시용) + const totalContracts = filteredProjects.reduce((sum, project) => sum + project.contracts.length, 0) return ( - <Popover open={open} onOpenChange={setOpen}> + <Popover open={popoverOpen} onOpenChange={setPopoverOpen}> <PopoverTrigger asChild> <Button + type="button" variant="outline" - role="combobox" - aria-expanded={open} - aria-label="Select a package" - className={cn("w-full justify-between", isCollapsed && "w-[50px]")} + className={cn( + "justify-between relative", + isCollapsed ? "h-9 w-9 shrink-0 items-center justify-center p-0" : "w-full h-9" + )} + disabled={isLoading} + aria-label="Select Contract" > - {isCollapsed ? ( - <ChevronsUpDown className="h-4 w-4" /> + {isLoading ? ( + <> + <span className={cn(isCollapsed && "hidden")}>Loading...</span> + <Loader2 className={cn("h-4 w-4 animate-spin", !isCollapsed && "ml-2")} /> + </> ) : ( <> - <span className="truncate">{displayText}</span> - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + <span className={cn("truncate flex-grow text-left", isCollapsed && "hidden")}> + {triggerLabel} + </span> + <ChevronsUpDown className={cn("h-4 w-4 opacity-50 flex-shrink-0", isCollapsed && "hidden")} /> </> )} </Button> </PopoverTrigger> - <PopoverContent className="w-[300px] p-0"> + + <PopoverContent className="w-[320px] p-0" align="start"> <Command> - <CommandInput placeholder="Search package..." /> - <CommandList> - <CommandEmpty>No package found.</CommandEmpty> - {projects.map((project) => ( - <CommandGroup key={project.projectId} heading={project.projectName}> - {project.packages.map((pkg) => ( + <CommandInput + placeholder="Search contracts..." + value={searchTerm} + onValueChange={setSearchTerm} + /> + + <CommandList + className="max-h-[320px]" + onWheel={(e) => { + e.stopPropagation() // 이벤트 전파 차단 + const target = e.currentTarget + target.scrollTop += e.deltaY // 직접 스크롤 처리 + }} + > + <CommandEmpty> + {totalContracts === 0 ? "No contracts found." : "No search results."} + </CommandEmpty> + + {filteredProjects.map((project) => ( + <CommandGroup key={project.projectCode} heading={project.projectName}> + {project.contracts.map((contract) => ( <CommandItem - key={`${project.projectId}-${pkg.packageCode}`} - onSelect={() => { - onSelectPackage(project.projectId, pkg.packageCode) - setOpen(false) - }} - className="text-sm" + key={contract.contractId} + onSelect={() => handleSelectContract(project.projectId, contract.contractId)} + value={`${project.projectName} ${contract.contractName}`} + className="truncate" + title={contract.contractName} > + <span className="truncate">{contract.contractName}</span> <Check className={cn( - "mr-2 h-4 w-4", - selectedProjectId === project.projectId && - selectedPackageCode === pkg.packageCode - ? "opacity-100" - : "opacity-0" + "ml-auto h-4 w-4 flex-shrink-0", + selectedContractId === contract.contractId ? "opacity-100" : "opacity-0" )} /> - <div className="flex flex-col"> - <span className="font-medium">{pkg.packageCode}</span> - {pkg.packageName && ( - <span className="text-xs text-muted-foreground"> - {pkg.packageName} - </span> - )} - </div> </CommandItem> ))} </CommandGroup> diff --git a/components/vendor-data-plant/sidebar.tsx b/components/vendor-data-plant/sidebar.tsx index b746e69d..31ee6dc7 100644 --- a/components/vendor-data-plant/sidebar.tsx +++ b/components/vendor-data-plant/sidebar.tsx @@ -10,265 +10,304 @@ import { TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip" -import { List, FormInput, FileText } from "lucide-react" +import { Package2, FormInput } from "lucide-react" +import { useRouter, usePathname } from "next/navigation" import { Skeleton } from "@/components/ui/skeleton" -import { getEngineeringForms, getIMForms } from "@/lib/tags-plant/service" +import { type FormInfo } from "@/lib/forms/services" -interface FormInfo { - formCode: string - formName: string +interface PackageData { + itemId: number + itemName: string } interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> { isCollapsed: boolean - selectedPackageCode: string | null - selectedFormCode: string | null - currentMode: "master" | "engineering" | "im" | null - projectCode: string // 추가 - onMasterTagListClick: () => void - onEngineeringFormClick: (formCode: string) => void - onIMFormClick: (formCode: string) => void + packages: PackageData[] + selectedPackageId: number | null + selectedProjectId: number | null + selectedContractId: number | null + onSelectPackage: (itemId: number) => void + forms?: FormInfo[] + onSelectForm: (formName: string) => void + isLoadingForms?: boolean + mode: "IM" | "ENG" } export function Sidebar({ className, isCollapsed, - selectedPackageCode, - selectedFormCode, - currentMode, - projectCode, // 추가 - onMasterTagListClick, - onEngineeringFormClick, - onIMFormClick, + packages, + selectedPackageId, + selectedProjectId, + selectedContractId, + onSelectPackage, + forms, + onSelectForm, + isLoadingForms = false, + mode = "IM", }: SidebarProps) { - const [engineeringForms, setEngineeringForms] = React.useState<FormInfo[]>([]) - const [imForms, setIMForms] = React.useState<FormInfo[]>([]) - const [isLoadingEngineering, setIsLoadingEngineering] = React.useState(false) - const [isLoadingIM, setIsLoadingIM] = React.useState(false) + const router = useRouter() + const rawPathname = usePathname() + const pathname = rawPathname ?? "" - // Engineering 폼 로드 - React.useEffect(() => { - if (!selectedPackageCode || !projectCode) { - setEngineeringForms([]) - return - } + /** + * --------------------------- + * 1) URL에서 현재 패키지 / 폼 코드 추출 + * --------------------------- + */ + const segments = pathname.split("/").filter(Boolean) - const loadEngineeringForms = async () => { - setIsLoadingEngineering(true) - try { - const result = await getEngineeringForms(projectCode, selectedPackageCode) - setEngineeringForms(result) - } catch (error) { - console.error("Engineering 폼 로딩 오류:", error) - setEngineeringForms([]) - } finally { - setIsLoadingEngineering(false) - } - } + let currentItemId: number | null = null + let currentFormCode: string | null = null - loadEngineeringForms() - }, [selectedPackageCode, projectCode]) + const tagIndex = segments.indexOf("tag") + if (tagIndex !== -1 && segments[tagIndex + 1]) { + currentItemId = parseInt(segments[tagIndex + 1], 10) + } - // IM 폼 로드 - React.useEffect(() => { - if (!selectedPackageCode || !projectCode) { - setIMForms([]) - return - } + const formIndex = segments.indexOf("form") + if (formIndex !== -1) { + const itemSegment = segments[formIndex + 1] + const codeSegment = segments[formIndex + 2] - const loadIMForms = async () => { - setIsLoadingIM(true) - try { - const result = await getIMForms(projectCode, selectedPackageCode) - setIMForms(result) - } catch (error) { - console.error("IM 폼 로딩 오류:", error) - setIMForms([]) - } finally { - setIsLoadingIM(false) - } + if (itemSegment) { + currentItemId = parseInt(itemSegment, 10) + } + if (codeSegment) { + currentFormCode = codeSegment } + } - loadIMForms() - }, [selectedPackageCode, projectCode]) + /** + * --------------------------- + * 2) 패키지 클릭 핸들러 (IM 모드) + * --------------------------- + */ + const handlePackageClick = (itemId: number) => { + // 상위 컴포넌트 상태 업데이트 + onSelectPackage(itemId) - const isMasterActive = currentMode === "master" - const isPackageSelected = selectedPackageCode !== null + // 해당 태그 페이지로 라우팅 + // 예: /vendor-data-plant/tag/123 + const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/tag/${itemId}`) + } + + /** + * --------------------------- + * 3) 폼 클릭 핸들러 (IM 모드만 사용) + * --------------------------- + */ + const handleFormClick = (form: FormInfo) => { + // IM 모드에서만 사용 + if (selectedPackageId === null) return; + + onSelectForm(form.formName) + + const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/${selectedPackageId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`) + } + + /** + * --------------------------- + * 4) 패키지 클릭 핸들러 (ENG 모드) + * --------------------------- + */ + const handlePackageUnderFormClick = (form: FormInfo, pkg: PackageData) => { + onSelectForm(form.formName) + onSelectPackage(pkg.itemId) + + const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/${pkg.itemId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`) + } return ( <div className={cn("pb-12", className)}> <div className="space-y-4 py-4"> - {/* Master Tag List */} - <div className="py-1"> - <h2 className="relative px-7 text-lg font-semibold tracking-tight"> - {isCollapsed ? "M" : "Master"} - </h2> - <div className="space-y-1 p-2"> - {isCollapsed ? ( - <Tooltip delayDuration={0}> - <TooltipTrigger asChild> - <Button - variant="ghost" - className={cn( - "w-full justify-start font-normal", - isMasterActive && "bg-accent text-accent-foreground" - )} - onClick={onMasterTagListClick} - disabled={!isPackageSelected} - > - <List className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent side="right"> - Master Tag List - </TooltipContent> - </Tooltip> - ) : ( - <Button - variant="ghost" - className={cn( - "w-full justify-start font-normal", - isMasterActive && "bg-accent text-accent-foreground" - )} - onClick={onMasterTagListClick} - disabled={!isPackageSelected} - > - <List className="mr-2 h-4 w-4" /> - Master Tag List - </Button> - )} - </div> - </div> + {/* ---------- 패키지(Items) 목록 - IM 모드에서만 표시 ---------- */} + {mode === "IM" && ( + <> + <div className="py-1"> + <h2 className="relative px-7 text-lg font-semibold tracking-tight"> + {isCollapsed ? "P" : "Package Lists"} + </h2> + <ScrollArea className="h-[150px] px-1"> + <div className="space-y-1 p-2"> + {packages.map((pkg) => { + const isActive = pkg.itemId === currentItemId - <Separator /> + return ( + <div key={pkg.itemId}> + {isCollapsed ? ( + <Tooltip delayDuration={0}> + <TooltipTrigger asChild> + <Button + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isActive && "bg-accent text-accent-foreground" + )} + onClick={() => handlePackageClick(pkg.itemId)} + > + <Package2 className="mr-2 h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="right"> + {pkg.itemName} + </TooltipContent> + </Tooltip> + ) : ( + <Button + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isActive && "bg-accent text-accent-foreground" + )} + onClick={() => handlePackageClick(pkg.itemId)} + > + <Package2 className="mr-2 h-4 w-4" /> + {pkg.itemName} + </Button> + )} + </div> + ) + })} + </div> + </ScrollArea> + </div> + <Separator /> + </> + )} - {/* Engineering Forms */} + {/* ---------- 폼 목록 (IM 모드) / 패키지와 폼 목록 (ENG 모드) ---------- */} <div className="py-1"> <h2 className="relative px-7 text-lg font-semibold tracking-tight"> - {isCollapsed ? "E" : "Engineering"} + {isCollapsed + ? (mode === "IM" ? "F" : "P") + : (mode === "IM" ? "Form Lists" : "Package Lists") + } </h2> - <ScrollArea className="h-[250px] px-1"> + <ScrollArea className={cn( + "px-1", + mode === "IM" ? "h-[300px]" : "h-[450px]" + )}> <div className="space-y-1 p-2"> - {isLoadingEngineering ? ( + {isLoadingForms ? ( Array.from({ length: 3 }).map((_, index) => ( - <div key={`eng-skeleton-${index}`} className="px-2 py-1.5"> + <div key={`form-skeleton-${index}`} className="px-2 py-1.5"> <Skeleton className="h-8 w-full" /> </div> )) - ) : !isPackageSelected ? ( - <p className="text-sm text-muted-foreground px-2"> - Select a package first - </p> - ) : engineeringForms.length === 0 ? ( - <p className="text-sm text-muted-foreground px-2"> - No forms available - </p> - ) : ( - engineeringForms.map((form) => { - const isActive = - currentMode === "engineering" && - form.formCode === selectedFormCode + ) : mode === "IM" ? ( + // =========== IM 모드: 폼만 표시 =========== + !forms || forms.length === 0 ? ( + <p className="text-sm text-muted-foreground px-2"> + (No forms loaded) + </p> + ) : ( + forms.map((form) => { + const isFormActive = form.formCode === currentFormCode + const isDisabled = currentItemId === null - return isCollapsed ? ( - <Tooltip key={form.formCode} delayDuration={0}> - <TooltipTrigger asChild> - <Button - variant="ghost" - className={cn( - "w-full justify-start font-normal", - isActive && "bg-accent text-accent-foreground" - )} - onClick={() => onEngineeringFormClick(form.formCode)} - > - <FormInput className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent side="right"> + return isCollapsed ? ( + <Tooltip key={form.formCode} delayDuration={0}> + <TooltipTrigger asChild> + <Button + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isFormActive && "bg-accent text-accent-foreground" + )} + onClick={() => handleFormClick(form)} + disabled={isDisabled} + > + <FormInput className="mr-2 h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="right"> + {form.formName} + </TooltipContent> + </Tooltip> + ) : ( + <Button + key={form.formCode} + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isFormActive && "bg-accent text-accent-foreground" + )} + onClick={() => handleFormClick(form)} + disabled={isDisabled} + > + <FormInput className="mr-2 h-4 w-4" /> {form.formName} - </TooltipContent> - </Tooltip> - ) : ( - <Button - key={form.formCode} - variant="ghost" - className={cn( - "w-full justify-start font-normal", - isActive && "bg-accent text-accent-foreground" - )} - onClick={() => onEngineeringFormClick(form.formCode)} - > - <FormInput className="mr-2 h-4 w-4" /> - {form.formName} - </Button> - ) - }) - )} - </div> - </ScrollArea> - </div> - - <Separator /> - - {/* IM Forms */} - <div className="py-1"> - <h2 className="relative px-7 text-lg font-semibold tracking-tight"> - {isCollapsed ? "I" : "IM"} - </h2> - <ScrollArea className="h-[250px] px-1"> - <div className="space-y-1 p-2"> - {isLoadingIM ? ( - Array.from({ length: 3 }).map((_, index) => ( - <div key={`im-skeleton-${index}`} className="px-2 py-1.5"> - <Skeleton className="h-8 w-full" /> - </div> - )) - ) : !isPackageSelected ? ( - <p className="text-sm text-muted-foreground px-2"> - Select a package first - </p> - ) : imForms.length === 0 ? ( - <p className="text-sm text-muted-foreground px-2"> - No forms available - </p> + </Button> + ) + }) + ) ) : ( - imForms.map((form) => { - const isActive = - currentMode === "im" && - form.formCode === selectedFormCode + // =========== ENG 모드: 패키지 > 폼 계층 구조 =========== + packages.length === 0 ? ( + <p className="text-sm text-muted-foreground px-2"> + (No packages loaded) + </p> + ) : ( + packages.map((pkg) => ( + <div key={pkg.itemId} className="space-y-1"> + {isCollapsed ? ( + <Tooltip delayDuration={0}> + <TooltipTrigger asChild> + <div className="px-2 py-1"> + <Package2 className="h-4 w-4" /> + </div> + </TooltipTrigger> + <TooltipContent side="right"> + {pkg.itemName} + </TooltipContent> + </Tooltip> + ) : ( + <> + {/* 패키지 이름 (클릭 불가능한 라벨) */} + <div className="flex items-center px-2 py-1 text-sm font-medium"> + <Package2 className="mr-2 h-4 w-4" /> + {pkg.itemName} + </div> + + {/* 폼 목록 바로 표시 */} + <div className="ml-6 space-y-1"> + {!forms || forms.length === 0 ? ( + <p className="text-xs text-muted-foreground px-2 py-1"> + No forms available + </p> + ) : ( + forms.map((form) => { + const isFormPackageActive = + pkg.itemId === currentItemId && + form.formCode === currentFormCode - return isCollapsed ? ( - <Tooltip key={form.formCode} delayDuration={0}> - <TooltipTrigger asChild> - <Button - variant="ghost" - className={cn( - "w-full justify-start font-normal", - isActive && "bg-accent text-accent-foreground" - )} - onClick={() => onIMFormClick(form.formCode)} - > - <FileText className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent side="right"> - {form.formName} - </TooltipContent> - </Tooltip> - ) : ( - <Button - key={form.formCode} - variant="ghost" - className={cn( - "w-full justify-start font-normal", - isActive && "bg-accent text-accent-foreground" + return ( + <Button + key={`${pkg.itemId}-${form.formCode}`} + variant="ghost" + size="sm" + className={cn( + "w-full justify-start font-normal text-sm", + isFormPackageActive && "bg-accent text-accent-foreground" + )} + onClick={() => handlePackageUnderFormClick(form, pkg)} + > + <FormInput className="mr-2 h-3 w-3" /> + {form.formName} + </Button> + ) + }) + )} + </div> + </> )} - onClick={() => onIMFormClick(form.formCode)} - > - <FileText className="mr-2 h-4 w-4" /> - {form.formName} - </Button> - ) - }) + </div> + )) + ) )} </div> </ScrollArea> diff --git a/components/vendor-data-plant/vendor-data-container.tsx b/components/vendor-data-plant/vendor-data-container.tsx index 7ce831df..60ec2c94 100644 --- a/components/vendor-data-plant/vendor-data-container.tsx +++ b/components/vendor-data-plant/vendor-data-container.tsx @@ -4,14 +4,28 @@ import * as React from "react" import { TooltipProvider } from "@/components/ui/tooltip" 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 } from "next/navigation" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services" import { Separator } from "@/components/ui/separator" -import { ProjectSwitcher } from "./project-swicher" +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' interface PackageData { - packageCode: string - packageName: string | null + itemId: number + itemName: string +} + +interface ContractData { + contractId: number + contractName: string + packages: PackageData[] } interface ProjectData { @@ -19,7 +33,7 @@ interface ProjectData { projectCode: string projectName: string projectType: string - packages: PackageData[] + contracts: ContractData[] } interface VendorDataContainerProps { @@ -30,39 +44,18 @@ interface VendorDataContainerProps { children: React.ReactNode } -function getInfoFromPathname(path: string | null): { - projectCode: string | null - packageCode: string | null - formCode: string | null - mode: "master" | "engineering" | "im" | null -} { - if (!path) return { projectCode: null, packageCode: null, formCode: null, mode: null } - - const segments = path.split("/").filter(Boolean) - const vendorDataIndex = segments.indexOf("vendor-data-plant") - - if (vendorDataIndex === -1) { - return { projectCode: null, packageCode: null, formCode: null, mode: null } - } +function getTagIdFromPathname(path: string | null): number | null { + if (!path) return null; - const projectCode = segments[vendorDataIndex + 1] || null - const packageCode = segments[vendorDataIndex + 2] || null + // 태그 패턴 검사 (/tag/123) + const tagMatch = path.match(/\/tag\/(\d+)/) + if (tagMatch) return parseInt(tagMatch[1], 10) - // /eng/{formCode} 또는 /im/{formCode} 패턴 체크 - const modeSegment = segments[vendorDataIndex + 3] - const formCode = segments[vendorDataIndex + 4] || null + // 폼 패턴 검사 (/form/123/...) + const formMatch = path.match(/\/form\/(\d+)/) + if (formMatch) return parseInt(formMatch[1], 10) - let mode: "master" | "engineering" | "im" | null = null - - if (modeSegment === "eng") { - mode = "engineering" - } else if (modeSegment === "im") { - mode = "im" - } else if (projectCode && packageCode && !modeSegment) { - mode = "master" - } - - return { projectCode, packageCode, formCode, mode } + return null } export function VendorDataContainer({ @@ -74,106 +67,267 @@ export function VendorDataContainer({ }: VendorDataContainerProps) { const pathname = usePathname() const router = useRouter() + const searchParams = useSearchParams() - // 상태 관리 + const tagIdNumber = getTagIdFromPathname(pathname) + + // 기본 상태 const [selectedProjectId, setSelectedProjectId] = React.useState(projects[0]?.projectId || 0) const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed) - const [selectedPackageCode, setSelectedPackageCode] = React.useState<string | null>(null) + const [selectedContractId, setSelectedContractId] = React.useState( + 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 [currentMode, setCurrentMode] = React.useState<"master" | "engineering" | "im" | null>(null) + const [isLoadingForms, setIsLoadingForms] = React.useState(false) - // 현재 선택된 프로젝트 + console.log(selectedPackageId,"selectedPackageId") + + + // 현재 선택된 프로젝트/계약/패키지 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" + + const [selectedMode, setSelectedMode] = useAtom(selectedModeAtom) + + // URL에서 모드 추출 (ship 프로젝트면 무조건 ENG로, 아니면 URL 또는 기본값) + const modeFromUrl = searchParams?.get('mode') + const initialMode ="ENG" + + // 모드 초기화 (기존의 useState 초기값 대신) + 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 변경 시 상태 동기화 + // URL에서 모드가 변경되면 상태도 업데이트 (ship 프로젝트가 아닐 때만) React.useEffect(() => { - const { projectCode, packageCode, formCode, mode } = getInfoFromPathname(pathname) - - if (projectCode && packageCode) { - // 프로젝트 찾기 - const project = projects.find(p => p.projectCode === projectCode) - if (project) { - setSelectedProjectId(project.projectId) - setSelectedPackageCode(packageCode) + if (!isShipProject) { + const modeFromUrl = searchParams?.get('mode') + if (modeFromUrl === "ENG" || modeFromUrl === "IM") { + setSelectedMode(modeFromUrl) } } - - if (formCode) { - setSelectedFormCode(formCode) - } else { - setSelectedFormCode(null) + }, [searchParams, isShipProject]) + + // 프로젝트 타입이 변경될 때 모드 업데이트 + React.useEffect(() => { + if (isShipProject) { + setSelectedMode("ENG") + + // URL 모드 파라미터도 업데이트 + const url = new URL(window.location.href); + url.searchParams.set('mode', 'ENG'); + router.replace(url.pathname + url.search); } - - if (mode) { - setCurrentMode(mode) + }, [isShipProject, router]) + + // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅 + React.useEffect(() => { + if (!currentContract) return + + if (tagIdNumber) { + setSelectedPackageId(tagIdNumber) } else { - setCurrentMode(null) + // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로 + if (currentContract.packages?.length) { + setSelectedPackageId(currentContract.packages[0].itemId) + } else { + setSelectedPackageId(null) + } } - }, [pathname, projects]) - - // 베이스 URL 생성 헬퍼 - const getBaseUrl = () => { - const segments = pathname?.split("/").filter(Boolean) || [] - const vendorDataIndex = segments.indexOf("vendor-data-plant") - if (vendorDataIndex === -1) return "" - return "/" + segments.slice(0, vendorDataIndex + 1).join("/") - } + }, [tagIdNumber, currentContract]) - // 프로젝트 및 패키지 선택 핸들러 - const handleSelectPackage = (projectId: number, packageCode: string) => { - const project = projects.find(p => p.projectId === projectId) - if (!project) return - - setSelectedProjectId(projectId) - setSelectedPackageCode(packageCode) - setSelectedFormCode(null) - setCurrentMode("master") + // (2) 프로젝트 변경 시 계약 초기화 + // React.useEffect(() => { + // if (currentProject?.contracts.length) { + // setSelectedContractId(currentProject.contracts[0].contractId) + // } else { + // setSelectedContractId(0) + // } + // }, [currentProject]) + + // (3) 패키지 ID와 모드가 변경될 때마다 폼 로딩 + React.useEffect(() => { + const packageId = getTagIdFromPathname(pathname) - const baseUrl = getBaseUrl() - router.push(`${baseUrl}/${project.projectCode}/${packageCode}`) - } + 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]) - // Master Tag List 클릭 핸들러 - const handleMasterTagListClick = () => { - if (!selectedPackageCode) return + // 모드에 따른 폼 로드 함수 + const loadFormsList = async (packageId: number, mode: "IM" | "ENG") => { + if (!packageId) return; - const project = projects.find(p => p.projectId === selectedProjectId) - if (!project) return + setIsLoadingForms(true); + try { + const result = await getFormsByContractItemId(packageId, mode); + setFormList(result.forms || []); + } catch (error) { + console.error(`폼 로딩 오류 (${mode} 모드):`, error); + setFormList([]); + } finally { + setIsLoadingForms(false); + } + }; + + // 핸들러들 +// 수정된 handleSelectContract 함수 +async function handleSelectContract(projId: number, cId: number) { + setSelectedProjectId(projId) + setSelectedContractId(cId) + + // 선택된 계약의 첫 번째 패키지 찾기 + const selectedProject = projects.find(p => p.projectId === projId) + const selectedContract = selectedProject?.contracts.find(c => c.contractId === cId) + + if (selectedContract?.packages?.length) { + const firstPackageId = selectedContract.packages[0].itemId + setSelectedPackageId(firstPackageId) - setCurrentMode("master") + // ENG 모드로 폼 목록 로드 + setIsLoadingForms(true) + try { + const result = await getFormsByContractItemId(firstPackageId, "ENG") + setFormList(result.forms || []) + + // 첫 번째 폼이 있으면 자동 선택 및 네비게이션 + if (result.forms && result.forms.length > 0) { + const firstForm = result.forms[0] + setSelectedFormCode(firstForm.formCode) + + // ENG 모드로 설정 + setSelectedMode("ENG") + + // 첫 번째 폼으로 네비게이션 + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${projId}/${cId}?mode=ENG`) + } else { + // 폼이 없는 경우에도 ENG 모드로 설정 + setSelectedMode("ENG") + setSelectedFormCode(null) + + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/0/0/${projId}/${cId}?mode=ENG`) + } + } catch (error) { + console.error("폼 로딩 오류:", error) + setFormList([]) + setSelectedFormCode(null) + + // 오류 발생 시에도 ENG 모드로 설정 + setSelectedMode("ENG") + } finally { + setIsLoadingForms(false) + } + } else { + // 패키지가 없는 경우 + setSelectedPackageId(null) + setFormList([]) setSelectedFormCode(null) - - const baseUrl = getBaseUrl() - router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}`) + setSelectedMode("ENG") } - - // Engineering 폼 클릭 핸들러 - const handleEngineeringFormClick = (formCode: string) => { - if (!selectedPackageCode) return - - const project = projects.find(p => p.projectId === selectedProjectId) - if (!project) return - - setCurrentMode("engineering") - setSelectedFormCode(formCode) - - const baseUrl = getBaseUrl() - router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/eng/${formCode}`) +} + + function handleSelectPackage(itemId: number) { + setSelectedPackageId(itemId) } + + function handleSelectForm(formName: string) { + const form = formList.find((f) => f.formName === formName) + if (form) { + setSelectedFormCode(form.formCode) + } + } + + // 모드 변경 핸들러 +// 모드 변경 핸들러 +const handleModeChange = async (mode: "IM" | "ENG") => { + // ship 프로젝트인 경우 모드 변경 금지 + if (isShipProject && mode !== "ENG") return; - // IM 폼 클릭 핸들러 - const handleIMFormClick = (formCode: string) => { - if (!selectedPackageCode) return - - const project = projects.find(p => p.projectId === selectedProjectId) - if (!project) return - - setCurrentMode("im") - setSelectedFormCode(formCode) + setSelectedMode(mode); + + // 모드가 변경될 때 자동 네비게이션 + if (currentContract?.packages?.length) { + const firstPackageId = currentContract.packages[0].itemId; - const baseUrl = getBaseUrl() - router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/im/${formCode}`) + if (mode === "IM") { + // IM 모드: 첫 번째 패키지로 이동 + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/"); + router.push(`/${baseSegments}/tag/${firstPackageId}?mode=${mode}`); + } else { + // ENG 모드: 폼 목록을 먼저 로드 + setIsLoadingForms(true); + try { + const result = await getFormsByContractItemId(firstPackageId, mode); + setFormList(result.forms || []); + + // 폼이 있으면 첫 번째 폼으로 이동 + 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-plant") + 1).join("/"); + router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`); + } else { + // 폼이 없으면 모드만 변경 + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/"); + router.push(`/${baseSegments}/form/0/0/${selectedProjectId}/${selectedContractId}?mode=${mode}`); + } + } 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); + } + } + } else { + // 패키지가 없는 경우, 모드만 변경 + const url = new URL(window.location.href); + url.searchParams.set('mode', mode); + router.replace(url.pathname + url.search); } +}; return ( <TooltipProvider delayDuration={0}> @@ -197,28 +351,151 @@ export function VendorDataContainer({ <ProjectSwitcher isCollapsed={isCollapsed} projects={projects} - selectedProjectId={selectedProjectId} - selectedPackageCode={selectedPackageCode} - onSelectPackage={handleSelectPackage} + selectedContractId={selectedContractId} + onSelectContract={handleSelectContract} /> </div> <Separator /> - <Sidebar - isCollapsed={isCollapsed} - selectedPackageCode={selectedPackageCode} - selectedFormCode={selectedFormCode} - currentMode={currentMode} - onMasterTagListClick={handleMasterTagListClick} - onEngineeringFormClick={handleEngineeringFormClick} - onIMFormClick={handleIMFormClick} - /> + {!isCollapsed ? ( + isShipProject ? ( + // 프로젝트 타입이 ship인 경우: 탭 없이 ENG 모드 사이드바만 바로 표시 + <div className="mt-0"> + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + 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" + /> + </div> + ) : ( + // 프로젝트 타입이 ship이 아닌 경우: 기존 탭 UI 표시 + <Tabs + defaultValue={initialMode} + value={selectedMode} + onValueChange={(value) => handleModeChange(value as "IM" | "ENG")} + className="w-full" + > + <TabsList className="w-full"> + <TabsTrigger value="ENG" className="flex-1">Engineering</TabsTrigger> + <TabsTrigger value="IM" className="flex-1">Handover</TabsTrigger> + + </TabsList> + + <TabsContent value="IM" className="mt-0"> + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + 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" + /> + </TabsContent> + + <TabsContent value="ENG" className="mt-0"> + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + 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" + /> + </TabsContent> + </Tabs> + ) + ) : ( + // 접혀있을 때 UI + <> + {!isShipProject && ( + // ship 프로젝트가 아닐 때만 모드 선택 버튼 표시 + <div className="flex justify-center space-x-1 my-2"> + + <Button + variant={selectedMode === "ENG" ? "default" : "ghost"} + size="sm" + className="h-8 px-2" + onClick={() => handleModeChange("ENG")} + > + Engineering + </Button> + <Button + variant={selectedMode === "IM" ? "default" : "ghost"} + size="sm" + className="h-8 px-2" + onClick={() => handleModeChange("IM")} + > + Handover + </Button> + </div> + )} + + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + 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" + /> + </> + )} </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize={defaultLayout[1]} minSize={40}> <div className="p-4 h-full overflow-auto flex flex-col"> + <div className="flex items-center justify-between mb-4"> + <h2 className="text-lg font-bold"> + {isShipProject || selectedMode === "ENG" + ? "Engineering Mode" + : `Package: ${currentPackageName}`} + </h2> + </div> {children} </div> </ResizablePanel> diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts index 5301e61a..c3df6b53 100644 --- a/db/schema/vendorData.ts +++ b/db/schema/vendorData.ts @@ -41,27 +41,6 @@ export const forms = pgTable("forms", { } }) -export const formsPlant = pgTable("forms_plant", { - id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - projectCode: varchar("project_code", { length: 100 }).notNull(), - packageCode: varchar("package_code", { length: 100 }).notNull(), - formCode: varchar("form_code", { length: 100 }).notNull(), - formName: varchar("form_name", { length: 255 }).notNull(), - // source: varchar("source", { length: 255 }), - // 새로 추가된 칼럼: eng와 im - eng: boolean("eng").default(false).notNull(), - im: boolean("im").default(false).notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), -}, (table) => { - return { - projectItemFormCodeUnique: uniqueIndex("project_item_form_code_unique").on( - table.projectCode, - table.formCode - ), - } -}) - // formMetas에 projectId 추가 export const formMetas = pgTable("form_metas", { id: serial("id").primaryKey(), @@ -94,16 +73,6 @@ export const formEntries = pgTable("form_entries", { updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }) -export const formEntriesPlant = pgTable("form_entries_plant", { - id: serial("id").primaryKey(), - formCode: varchar("form_code", { length: 50 }).notNull(), - data: jsonb("data").notNull(), - projectCode: varchar("project_code", { length: 100 }).notNull(), - packageCode: varchar("package_code", { length: 100 }).notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}) - export const tags = pgTable("tags", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), contractItemId: integer("contract_item_id") @@ -139,42 +108,6 @@ export const tags = pgTable("tags", { }; }); - -export const tagsPlant = pgTable("tags_plant", { - id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - - // SEDP에서 오는 고유 식별자 - tagIdx: varchar("tag_idx", { length: 100 }).notNull(), - - // 사용자가 편집 가능한 태그 번호 - tagNo: varchar("tag_no", { length: 100 }).notNull(), - - tagType: varchar("tag_type", { length: 50 }).notNull(), - class: varchar("class", { length: 100 }).notNull(), - tagClassId: integer("tag_class_id") - .references(() => tagClasses.id, { onDelete: "set null" }), - description: text("description"), - - // 동적 속성을 저장할 JSONB 컬럼 추가 - attributes: jsonb("attributes").$type<Record<string, string>>(), - - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - - projectCode: varchar("project_code", { length: 100 }).notNull(), - packageCode: varchar("package_code", { length: 100 }).notNull(), - formId: integer("form_id"), - -}, (table) => { - return { - projectPackageTagIdxUnique: uniqueIndex("project_package_tag_idx_unique").on( - table.projectCode, - table.packageCode, - table.tagIdx - ), - } -}) - // tagTypes에 projectId 추가 및 복합 기본키 생성 export const tagTypes = pgTable("tag_types", { code: varchar("code", { length: 50 }).notNull(), @@ -401,26 +334,6 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", { export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect; -export const vendorDataReportTempsPlant = pgTable("vendor_data_report_temps_plant", { - id: serial("id").primaryKey(), - - projectCode: varchar("project_code", { length: 100 }).notNull(), - packageCode: varchar("package_code", { length: 100 }).notNull(), - formId: integer("form_id") - .notNull() - .references(() => forms.id, { onDelete: "cascade" }), - fileName: varchar("file_name", { length: 255 }).notNull(), - filePath: varchar("file_path", { length: 1024 }).notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), -}); - -export type VendorDataReportTempsPlant = typeof vendorDataReportTempsPlant.$inferSelect; - export const formListsView = pgView("form_lists_view").as((qb) => { return qb diff --git a/lib/forms-plant/services.ts b/lib/forms-plant/services.ts index 7e1976e6..219f36e4 100644 --- a/lib/forms-plant/services.ts +++ b/lib/forms-plant/services.ts @@ -7,18 +7,18 @@ import fs from "fs/promises"; import { v4 as uuidv4 } from "uuid"; import db from "@/db/db"; import { - formEntries,formEntriesPlant, - formMetas,formsPlant, + formEntries, + formMetas, forms, tagClassAttributes, tagClasses, - tags,tagsPlant, + tags, tagSubfieldOptions, tagSubfields, tagTypeClassFormMappings, tagTypes, - vendorDataReportTempsPlant, - VendorDataReportTempsPlant, + vendorDataReportTemps, + VendorDataReportTemps, } from "@/db/schema/vendorData"; import { eq, and, desc, sql, DrizzleError, inArray, or, type SQL, type InferSelectModel } from "drizzle-orm"; import { unstable_cache } from "next/cache"; @@ -29,7 +29,6 @@ import { contractItems, contracts, items, projects } from "@/db/schema"; import { getSEDPToken } from "../sedp/sedp-token"; import { decryptWithServerAction } from "@/components/drm/drmUtils"; import { deleteFile, saveFile } from "@/lib/file-stroage"; -import { Register } from "@/components/form-data-plant/form-data-table-columns"; export type FormInfo = InferSelectModel<typeof forms>; @@ -165,8 +164,7 @@ export interface EditableFieldsInfo { // TAG별 편집 가능 필드 조회 함수 export async function getEditableFieldsByTag( - projectCode: string, - packageCode: string, + contractItemId: number, projectId: number ): Promise<Map<string, string[]>> { try { @@ -176,11 +174,8 @@ export async function getEditableFieldsByTag( tagNo: tags.tagNo, tagClass: tags.class }) - .from(tagsPlant) - .where( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode), - ); + .from(tags) + .where(eq(tags.contractItemId, contractItemId)); const editableFieldsMap = new Map<string, string[]>(); @@ -233,17 +228,26 @@ export async function getEditableFieldsByTag( * data가 배열이면 그 배열을 반환, * 그리고 이 로직 전체를 unstable_cache로 감싸 캐싱. */ -export async function getFormData(formCode: string, projectCode: string, packageCode:string) { +export async function getFormData(formCode: string, contractItemId: number) { try { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 기존 로직으로 projectId, columns, data 가져오기 + const contractItemResult = await db + .select({ + projectId: projects.id + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contractItems.id, contractItemId)) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[getFormData] No contract item found with ID: ${contractItemId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } - const projectId = project.id; + const projectId = contractItemResult[0].projectId; const metaRows = await db .select() @@ -265,15 +269,14 @@ export async function getFormData(formCode: string, projectCode: string, package const entryRows = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1); const entry = entryRows[0] ?? null; @@ -318,7 +321,7 @@ export async function getFormData(formCode: string, projectCode: string, package } // *** 새로 추가: 편집 가능 필드 정보 계산 *** - const editableFieldsMap = await getEditableFieldsByTag(projectCode,packageCode ,projectId); + const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId); return { columns, data, editableFieldsMap }; @@ -328,16 +331,24 @@ export async function getFormData(formCode: string, projectCode: string, package // Fallback logic (기존과 동일하게 editableFieldsMap 추가) try { - console.log(`[getFormData] Fallback DB query for (${formCode}, ${packageCode})`); + console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`); - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + const contractItemResult = await db + .select({ + projectId: projects.id + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contractItems.id, contractItemId)) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[getFormData] Fallback: No contract item found with ID: ${contractItemId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } - const projectId = project.id; + const projectId = contractItemResult[0].projectId; const metaRows = await db .select() @@ -359,15 +370,14 @@ export async function getFormData(formCode: string, projectCode: string, package const entryRows = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1); const entry = entryRows[0] ?? null; @@ -396,7 +406,7 @@ export async function getFormData(formCode: string, projectCode: string, package } // Fallback에서도 편집 가능 필드 정보 계산 - const editableFieldsMap = await getEditableFieldsByTag(projectCode, packageCode, projectId); + const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId); return { columns, data, projectId, editableFieldsMap }; } catch (dbError) { @@ -405,7 +415,7 @@ export async function getFormData(formCode: string, projectCode: string, package } } } -/** +/**1 * contractId와 formCode(itemCode)를 사용하여 contractItemId를 찾는 서버 액션 * * @param contractId - 계약 ID @@ -507,26 +517,24 @@ export async function getPackageCodeById(contractItemId: number): Promise<string export async function syncMissingTags( - projectCode: string, - packageCode: string, + contractItemId: number, formCode: string ) { // (1) Ensure there's a row in `forms` matching (contractItemId, formCode). const [formRow] = await db .select() - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formCode) + eq(forms.contractItemId, contractItemId), + eq(forms.formCode, formCode) ) ) .limit(1); if (!formRow) { throw new Error( - `Form not found for projectCode=${projectCode}, formCode=${formCode}` + `Form not found for contractItemId=${contractItemId}, formCode=${formCode}` ); } @@ -550,28 +558,26 @@ export async function syncMissingTags( // (3) Fetch all matching `tags` for the contractItemId + any of the (tagType, class) pairs. const tagRows = await db .select() - .from(tagsPlant) - .where(and(eq(tagsPlant.packageCode, packageCode),eq(tagsPlant.projectCode, projectCode), or(...orConditions))); + .from(tags) + .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions))); // (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode). let [entry] = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.formCode, formCode) + eq(formEntries.contractItemId, contractItemId), + eq(formEntries.formCode, formCode) ) ) .limit(1); if (!entry) { const [inserted] = await db - .insert(formEntriesPlant) + .insert(formEntries) .values({ - projectCode, - packageCode, + contractItemId, formCode, data: [], // Initialize with empty array }) @@ -640,13 +646,13 @@ export async function syncMissingTags( // (6) 실제로 추가되거나 수정되거나 삭제된 게 있다면 DB에 반영 if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) { await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); } // 캐시 무효화 등 후처리 - // revalidateTag(`form-data-${formCode}-${projectCode}`); + revalidateTag(`form-data-${formCode}-${contractItemId}`); return { createdCount, updatedCount, deletedCount }; } @@ -675,8 +681,7 @@ export interface UpdateResponse { export async function updateFormDataInDB( formCode: string, - projectCode: string, - packageCode: string, + contractItemId: number, newData: Record<string, any> ): Promise<UpdateResponse> { try { @@ -692,12 +697,11 @@ export async function updateFormDataInDB( // 2) row 찾기 (단 하나) const entries = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( eq(formEntries.formCode, formCode), - eq(formEntries.projectCode, projectCode), - eq(formEntries.packageCode, packageCode), + eq(formEntries.contractItemId, contractItemId) ) ) .limit(1); @@ -752,12 +756,12 @@ export async function updateFormDataInDB( // 6) DB UPDATE try { await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedArray, updatedAt: new Date(), // 업데이트 시간도 갱신 }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); } catch (dbError) { console.error("Database update error:", dbError); @@ -777,7 +781,7 @@ export async function updateFormDataInDB( // 7) Cache 무효화 try { // 캐시 태그를 form-data-${formCode}-${contractItemId} 형태로 가정 - const cacheTag = `form-data-${formCode}-${projectCode}`; + const cacheTag = `form-data-${formCode}-${contractItemId}`; console.log(cacheTag, "update") revalidateTag(cacheTag); } catch (cacheError) { @@ -810,8 +814,7 @@ export async function updateFormDataInDB( export async function updateFormDataBatchInDB( formCode: string, - projectCode: string, - packageCode: string, + contractItemId: number, newDataArray: Record<string, any>[] ): Promise<UpdateResponse> { try { @@ -836,12 +839,11 @@ export async function updateFormDataBatchInDB( // 1) DB에서 현재 데이터 가져오기 const entries = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) .limit(1); @@ -849,7 +851,7 @@ export async function updateFormDataBatchInDB( if (!entries || entries.length === 0) { return { success: false, - message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, projectCode=${projectCode})`, + message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`, }; } @@ -916,12 +918,12 @@ export async function updateFormDataBatchInDB( // 3) DB에 한 번만 저장 try { await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedArray, updatedAt: new Date(), }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); } catch (dbError) { console.error("Database update error:", dbError); @@ -950,7 +952,7 @@ export async function updateFormDataBatchInDB( // 4) 캐시 무효화 try { - const cacheTag = `form-data-${formCode}-${projectCode}`; + const cacheTag = `form-data-${formCode}-${contractItemId}`; console.log(`Cache invalidated: ${cacheTag}`); revalidateTag(cacheTag); } catch (cacheError) { @@ -1041,37 +1043,26 @@ export async function fetchFormMetadata( } type GetReportFileList = ( - projectCode: string, - packageCode: string, - formCode: string, - mode: string + packageId: string, + formCode: string ) => Promise<{ formId: number; }>; -export const getFormId: GetReportFileList = async (projectCode, packageCode, formCode, mode) => { +export const getFormId: GetReportFileList = async (packageId, formCode) => { const result: { formId: number } = { formId: 0, }; try { - // mode에 따른 조건 배열 생성 - const conditions = [ - eq(formsPlant.formCode, formCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - ]; - - // mode에 따라 추가 조건 설정 - if (mode === "IM") { - conditions.push(eq(formsPlant.im, true)); - } else if (mode === "ENG") { - conditions.push(eq(formsPlant.eng, true)); - } - const [targetForm] = await db .select() - .from(formsPlant) - .where(and(...conditions)); + .from(forms) + .where( + and( + eq(forms.formCode, formCode), + eq(forms.contractItemId, Number(packageId)) + ) + ); if (!targetForm) { throw new Error("Not Found Target Form"); @@ -1081,34 +1072,30 @@ export const getFormId: GetReportFileList = async (projectCode, packageCode, for result.formId = formId; } catch (err) { - console.error("Error getting form ID:", err); } finally { return result; } }; type getReportTempList = ( - projectCode: string, - packageCode: string, + packageId: number, formId: number -) => Promise<VendorDataReportTempsPlant[]>; +) => Promise<VendorDataReportTemps[]>; export const getReportTempList: getReportTempList = async ( - projectCode, - packageCode, + packageId, formId ) => { - let result: VendorDataReportTempsPlant[] = []; + let result: VendorDataReportTemps[] = []; try { result = await db .select() - .from(vendorDataReportTempsPlant) + .from(vendorDataReportTemps) .where( and( - eq(vendorDataReportTempsPlant.projectCode, projectCode), - eq(vendorDataReportTempsPlant.packageCode, packageCode), - eq(vendorDataReportTempsPlant.formId, formId) + eq(vendorDataReportTemps.contractItemId, packageId), + eq(vendorDataReportTemps.formId, formId) ) ); } catch (err) { @@ -1118,8 +1105,7 @@ export const getReportTempList: getReportTempList = async ( }; export async function uploadReportTemp( - projectCode: string, - packageCode: string, + packageId: number, formId: number, formData: FormData ) { @@ -1142,10 +1128,9 @@ export async function uploadReportTemp( return db.transaction(async (tx) => { // 파일 정보를 테이블에 저장 await tx - .insert(vendorDataReportTempsPlant) + .insert(vendorDataReportTemps) .values({ - projectCode, - packageCode, + contractItemId: packageId, formId: formId, fileName: customFileName, filePath: saveResult.publicPath!, @@ -1175,16 +1160,16 @@ export const deleteReportTempFile: deleteReportTempFile = async (id) => { return db.transaction(async (tx) => { const [targetTempFile] = await tx .select() - .from(vendorDataReportTempsPlant) - .where(eq(vendorDataReportTempsPlant.id, id)); + .from(vendorDataReportTemps) + .where(eq(vendorDataReportTemps.id, id)); if (!targetTempFile) { throw new Error("해당 Template File을 찾을 수 없습니다."); } await tx - .delete(vendorDataReportTempsPlant) - .where(eq(vendorDataReportTempsPlant.id, id)); + .delete(vendorDataReportTemps) + .where(eq(vendorDataReportTemps.id, id)); const { filePath } = targetTempFile; @@ -1325,8 +1310,7 @@ async function transformDataToSEDPFormat( formCode: string, objectCode: string, projectNo: string, - packageCode: string, - contractItemId: string, + contractItemId: number, // Add contractItemId parameter designerNo: string = "253213" ): Promise<SEDPDataItem[]> { // Create a map for quick column lookup @@ -1347,6 +1331,9 @@ async function transformDataToSEDPFormat( // Cache for UOM factors to avoid duplicate API calls const uomFactorCache = new Map<string, number>(); + // Cache for packageCode to avoid duplicate DB queries for same tag + const packageCodeCache = new Map<string, string>(); + // Cache for tagClass code to avoid duplicate DB queries for same tag const tagClassCodeCache = new Map<string, string>(); @@ -1354,16 +1341,98 @@ async function transformDataToSEDPFormat( const transformedItems = []; for (const row of tableData) { - let tagClassCode = ""; - // Get tagClass code if TAG_NO exists + const cotractItem = await db.query.contractItems.findFirst({ + where: + eq(contractItems.id, contractItemId), + }); + + const item = await db.query.items.findFirst({ + where: + eq(items.id, cotractItem.itemId), + }); + + // Get packageCode for this specific tag + let packageCode = item.packageCode; // fallback to formCode + let tagClassCode = ""; // for CLS_ID + if (row.TAG_NO && contractItemId) { + // Check cache first const cacheKey = `${contractItemId}-${row.TAG_NO}`; - if (tagClassCodeCache.has(cacheKey)) { - tagClassCode = tagClassCodeCache.get(cacheKey)!; + if (packageCodeCache.has(cacheKey)) { + packageCode = packageCodeCache.get(cacheKey)!; } else { try { + // Query to get packageCode for this specific tag + const tagResult = await db.query.tags.findFirst({ + where: and( + eq(tags.contractItemId, contractItemId), + eq(tags.tagNo, row.TAG_NO) + ) + }); + + if (tagResult) { + // Get tagClass code if tagClassId exists + if (tagResult.tagClassId) { + // Check tagClass cache first + if (tagClassCodeCache.has(cacheKey)) { + tagClassCode = tagClassCodeCache.get(cacheKey)!; + } else { + const tagClassResult = await db.query.tagClasses.findFirst({ + where: eq(tagClasses.id, tagResult.tagClassId) + }); + + if (tagClassResult) { + tagClassCode = tagClassResult.code; + console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`); + } else { + console.warn(`No tagClass found for tagClassId: ${tagResult.tagClassId}`); + } + + // Cache the tagClass code result + tagClassCodeCache.set(cacheKey, tagClassCode); + } + } + + // Get the contract item + const contractItemResult = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, tagResult.contractItemId) + }); + + if (contractItemResult) { + // Get the first item with this itemId + const itemResult = await db.query.items.findFirst({ + where: eq(items.id, contractItemResult.itemId) + }); + + if (itemResult && itemResult.packageCode) { + packageCode = itemResult.packageCode; + console.log(`Found packageCode for tag ${row.TAG_NO}: ${packageCode}`); + } else { + console.warn(`No item found for contractItem.itemId: ${contractItemResult.itemId}, using fallback`); + } + } else { + console.warn(`No contractItem found for tag ${row.TAG_NO}, using fallback`); + } + } else { + console.warn(`No tag found for contractItemId: ${contractItemId}, tagNo: ${row.TAG_NO}, using fallback`); + } + + // Cache the result (even if it's the fallback value) + packageCodeCache.set(cacheKey, packageCode); + } catch (error) { + console.error(`Error fetching packageCode for tag ${row.TAG_NO}:`, error); + // Use fallback value and cache it + packageCodeCache.set(cacheKey, packageCode); + } + } + + // Get tagClass code if not already retrieved above + if (!tagClassCode && tagClassCodeCache.has(cacheKey)) { + tagClassCode = tagClassCodeCache.get(cacheKey)!; + } else if (!tagClassCode) { + try { const tagResult = await db.query.tags.findFirst({ where: and( eq(tags.contractItemId, contractItemId), @@ -1371,20 +1440,22 @@ async function transformDataToSEDPFormat( ) }); - if (tagResult?.tagClassId) { + if (tagResult && tagResult.tagClassId) { const tagClassResult = await db.query.tagClasses.findFirst({ where: eq(tagClasses.id, tagResult.tagClassId) }); - + if (tagClassResult) { tagClassCode = tagClassResult.code; console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`); } } + // Cache the tagClass code result tagClassCodeCache.set(cacheKey, tagClassCode); } catch (error) { - console.error(`Error fetching tagClass for tag ${row.TAG_NO}:`, error); + console.error(`Error fetching tagClass code for tag ${row.TAG_NO}:`, error); + // Cache empty string as fallback tagClassCodeCache.set(cacheKey, ""); } } @@ -1395,16 +1466,17 @@ async function transformDataToSEDPFormat( TAG_NO: row.TAG_NO || "", TAG_DESC: row.TAG_DESC || "", ATTRIBUTES: [], + // SCOPE: objectCode, SCOPE: packageCode, - TOOLID: "eVCP", + TOOLID: "eVCP", // Changed from VDCS ITM_NO: row.TAG_NO || "", - OP_DELETE: row.status === "Deleted", + OP_DELETE: row.status === "Deleted", // Set OP_DELETE based on status MAIN_YN: true, LAST_REV_YN: true, CRTER_NO: designerNo, CHGER_NO: designerNo, - TYPE: formCode, - CLS_ID: tagClassCode, + TYPE: formCode, // Use packageCode instead of formCode + CLS_ID: tagClassCode, // Add CLS_ID with tagClass code PROJ_NO: projectNo, REV_NO: "00", CRTE_DTM: currentTimestamp, @@ -1450,7 +1522,7 @@ async function transformDataToSEDPFormat( const uomData = await response.json(); if (uomData && uomData.FACTOR !== undefined && uomData.FACTOR !== null) { factor = Number(uomData.FACTOR); - // Store in cache for future use + // Store in cache for future use (type assertion to ensure it's a number) uomFactorCache.set(column.uomId, factor); } } else { @@ -1461,7 +1533,7 @@ async function transformDataToSEDPFormat( } } - // Apply the factor if needed (currently commented out) + // Apply the factor if we got one // if (factor !== undefined && typeof value === 'number') { // value = value * factor; // } @@ -1469,7 +1541,7 @@ async function transformDataToSEDPFormat( const attribute: SEDPAttribute = { NAME: key, - VALUE: String(value), + VALUE: String(value), // 모든 값을 문자열로 변환 UOM: column?.uom || "", CLS_ID: tagClassCode || "", }; @@ -1497,7 +1569,7 @@ export async function transformFormDataToSEDP( formCode: string, objectCode: string, projectNo: string, - packageCode: string, // Add contractItemId parameter + contractItemId: number, // Add contractItemId parameter designerNo: string = "253213" ): Promise<SEDPDataItem[]> { return transformDataToSEDPFormat( @@ -1506,7 +1578,7 @@ export async function transformFormDataToSEDP( formCode, objectCode, projectNo, - packageCode, + contractItemId, // Pass contractItemId designerNo ); } @@ -1527,20 +1599,6 @@ export async function getProjectCodeById(projectId: number): Promise<string> { return projectRecord[0].code; } -export async function getProjectIdByCode(projectCode: string): Promise<number> { - const projectRecord = await db - .select({ id: projects.id }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectRecord || projectRecord.length === 0) { - throw new Error(`Project not found with ID: ${projectId}`); - } - - return projectRecord[0].id; -} - export async function getProjectById(projectId: number): Promise<{ code: string; type: string; }> { const projectRecord = await db .select({ code: projects.code , type:projects.type}) @@ -1621,13 +1679,13 @@ export async function sendDataToSEDP( export async function sendFormDataToSEDP( formCode: string, projectId: number, - projectCode: string, // contractItemId 파라미터 추가 - packageCode: string, // contractItemId 파라미터 추가 + contractItemId: number, // contractItemId 파라미터 추가 formData: GenericData[], columns: DataTableColumnJSON[] ): Promise<{ success: boolean; message: string; data?: any }> { try { // 1. Get project code + const projectCode = await getProjectCodeById(projectId); // 2. Get class mapping const mappingsResult = await db.query.tagTypeClassFormMappings.findFirst({ @@ -1670,7 +1728,7 @@ export async function sendFormDataToSEDP( formCode, objectCode, projectCode, - packageCode // Add contractItemId parameter + contractItemId // Add contractItemId parameter ); // 4. Send to SEDP API @@ -1681,12 +1739,11 @@ export async function sendFormDataToSEDP( // Get the current formEntries data const entries = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) .limit(1); @@ -1721,17 +1778,17 @@ export async function sendFormDataToSEDP( // Update the database await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedDataArray, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); console.log(`Updated status for ${sentTagNumbers.size} tags to "Sent to S-EDP"`); } } else { - console.warn(`No formEntriesPlant found for formCode: ${formCode}`); + console.warn(`No formEntries found for formCode: ${formCode}, contractItemId: ${contractItemId}`); } } catch (statusUpdateError) { // Status 업데이트 실패는 경고로만 처리 (SEDP 전송은 성공했으므로) @@ -1755,14 +1812,12 @@ export async function sendFormDataToSEDP( export async function deleteFormDataByTags({ formCode, - projectCode, - packageCode, + contractItemId, tagIdxs, projectId, }: { formCode: string - projectCode: string - packageCode: string + contractItemId: number tagIdxs: string[] projectId?: number }): Promise<{ @@ -1775,26 +1830,25 @@ export async function deleteFormDataByTags({ }> { try { // 입력 검증 - if (!formCode || !projectCode || !Array.isArray(tagIdxs) || tagIdxs.length === 0) { + if (!formCode || !contractItemId || !Array.isArray(tagIdxs) || tagIdxs.length === 0) { return { error: "Missing required parameters: formCode, contractItemId, tagIdxs", } } - console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, projectCode: ${projectCode}, tagIdxs:`, tagIdxs) + console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagIdxs:`, tagIdxs) // 1. 트랜잭션 전에 삭제할 항목들을 미리 조회하여 저장 (S-EDP 전송용) const entryForSedp = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1) let itemsToSendToSedp: Record<string, unknown>[] = [] @@ -1814,15 +1868,14 @@ export async function deleteFormDataByTags({ // 2-1. 현재 formEntry 데이터 가져오기 const currentEntryResult = await tx .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1) if (currentEntryResult.length === 0) { @@ -1850,15 +1903,14 @@ export async function deleteFormDataByTags({ // 2-3. tags 테이블에서 해당 태그들 삭제 const deletedTagsResult = await tx - .delete(tagsPlant) + .delete(tags) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode), - inArray(tagsPlant.tagIdx, tagIdxs) + eq(tags.contractItemId, contractItemId), + inArray(tags.tagIdx, tagIdxs) ) ) - .returning({ tagNo: tagsPlant.tagNo }) + .returning({ tagNo: tags.tagNo }) const deletedTagsCount = deletedTagsResult.length @@ -1867,16 +1919,15 @@ export async function deleteFormDataByTags({ // 2-4. formEntries 데이터 업데이트 (삭제된 항목 제외) await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date(), }) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) @@ -1969,8 +2020,7 @@ export async function deleteFormDataByTags({ const sedpResult = await sendFormDataToSEDP( formCode, projectId, - projectCode, - packageCode, + contractItemId, uniqueDeletedItems as GenericData[], formMetaResult.columns as DataTableColumnJSON[] ) @@ -2029,13 +2079,11 @@ export async function deleteFormDataByTags({ */ export async function excludeFormDataByTags({ formCode, - projectCode, - packageCode, + contractItemId, tagNumbers, }: { formCode: string - projectCode: string - packageCode: string + contractItemId: number tagNumbers: string[] }): Promise<{ error?: string @@ -2044,28 +2092,27 @@ export async function excludeFormDataByTags({ }> { try { // 입력 검증 - if (!formCode || !projectCode || !Array.isArray(tagNumbers) || tagNumbers.length === 0) { + if (!formCode || !contractItemId || !Array.isArray(tagNumbers) || tagNumbers.length === 0) { return { - error: "Missing required parameters: formCode, projectCode, tagNumbers", + error: "Missing required parameters: formCode, contractItemId, tagNumbers", } } - console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, projectCode: ${projectCode}, tagNumbers:`, tagNumbers) + console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagNumbers:`, tagNumbers) // 트랜잭션으로 안전하게 처리 const result = await db.transaction(async (tx) => { // 1. 현재 formEntry 데이터 가져오기 const currentEntryResult = await tx .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1) if (currentEntryResult.length === 0) { @@ -2099,16 +2146,15 @@ export async function excludeFormDataByTags({ // 3. formEntries 데이터 업데이트 await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date(), }) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) @@ -2119,7 +2165,7 @@ export async function excludeFormDataByTags({ }) // 4. 캐시 무효화 - const cacheKey = `form-data-${formCode}-${packageCode}` + const cacheKey = `form-data-${formCode}-${contractItemId}` revalidateTag(cacheKey) console.log(`[EXCLUDE ACTION] Transaction completed successfully`) diff --git a/lib/forms-plant/stat.ts b/lib/forms-plant/stat.ts index f734e782..f13bab61 100644 --- a/lib/forms-plant/stat.ts +++ b/lib/forms-plant/stat.ts @@ -1,7 +1,7 @@ "use server" import db from "@/db/db" -import { vendors, contracts, contractItems, forms,formsPlant,formEntriesPlant, formEntries, formMetas, tags,tagsPlant, tagClasses, tagClassAttributes, projects } from "@/db/schema" +import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema" import { eq, and, inArray } from "drizzle-orm" import { getEditableFieldsByTag } from "./services" import { getServerSession } from "next-auth/next" @@ -218,7 +218,7 @@ export async function getVendorFormStatus(projectId?: number): Promise<VendorFor -export async function getFormStatusByVendor(projectId: number, projectCode: string, packageCode: string, formCode: string): Promise<FormStatusByVendor[]> { +export async function getFormStatusByVendor(projectId: number, contractItemId: number, formCode: string): Promise<FormStatusByVendor[]> { try { const session = await getServerSession(authOptions) if (!session?.user?.id) { @@ -244,16 +244,15 @@ export async function getFormStatusByVendor(projectId: number, projectCode: stri // 4. contractItem별 forms 조회 const formsList = await db .select({ - id: formsPlant.id, - formCode: formsPlant.formCode, - contractItemId: formsPlant.contractItemId + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId }) - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formCode) + eq(forms.contractItemId, contractItemId), + eq(forms.formCode, formCode) ) ) @@ -262,21 +261,20 @@ export async function getFormStatusByVendor(projectId: number, projectCode: stri // 5. formEntries 조회 const entriesList = await db .select({ - id: formEntriesPlant.id, - formCode: formEntriesPlant.formCode, - data: formEntriesPlant.data + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data }) - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.formCode, formCode) + eq(formEntries.contractItemId, contractItemId), + eq(formEntries.formCode, formCode) ) ) // 6. TAG별 편집 가능 필드 조회 - const editableFieldsByTag = await getEditableFieldsByTag(projectCode,packageCode, projectId) + const editableFieldsByTag = await getEditableFieldsByTag(contractItemId, projectId) const vendorStatusList: VendorFormStatus[] = [] diff --git a/lib/sedp/get-form-tags-plant.ts b/lib/sedp/get-form-tags-plant.ts deleted file mode 100644 index 176f1b3f..00000000 --- a/lib/sedp/get-form-tags-plant.ts +++ /dev/null @@ -1,933 +0,0 @@ -import db from "@/db/db"; -import { - contractItems, - tagsPlant, - formsPlant,formEntriesPlant, - items, - tagTypeClassFormMappings, - projects, - tagTypes, - tagClasses, - formMetas, -} from "@/db/schema"; -import { eq, and, like, inArray } from "drizzle-orm"; -import { getSEDPToken } from "./sedp-token"; -import { getFormMappingsByTagTypebyProeject } from "../tags/form-mapping-service"; - - -interface Attribute { - ATT_ID: string; - VALUE: any; - VALUE_DBL: number; - UOM_ID: string | null; -} - -interface TagEntry { - TAG_IDX: string; - TAG_NO: string; - BF_TAG_NO: string; - TAG_DESC: string; - EP_ID: string; - TAG_TYPE_ID: string; - CLS_ID: string; - ATTRIBUTES: Attribute[]; - [key: string]: any; -} - -interface Column { - key: string; - label: string; - type: string; - shi?: string | null; -} - -interface newRegister { - PROJ_NO: string; - MAP_ID: string; - EP_ID: string; - CATEGORY: string; - BYPASS: boolean; - REG_TYPE_ID: string; - TOOL_ID: string; - TOOL_TYPE: string; - SCOPES: string[]; - MAP_CLS: { - TOOL_ATT_NAME: string; - ITEMS: ClassItmes[]; - }; - MAP_ATT: MapAttribute[]; - MAP_TMPLS: string[]; - CRTER_NO: string; - CRTE_DTM: string; - CHGER_NO: string; - _id: string; -} - -interface ClassItmes { - SEDP_OBJ_CLS_ID: string; - TOOL_VALS: string; - ISDEFALUT: boolean; -} - -interface MapAttribute { - SEDP_ATT_ID: string; - TOOL_ATT_NAME: string; - KEY_YN: boolean; - DUE_DATE: string; //"YYYY-MM-DDTHH:mm:ssZ" - INOUT: string | null; -} - - - -async function getNewRegisters(projectCode: string): Promise<newRegister[]> { - try { - // 토큰(API 키) 가져오기 - const apiKey = await getSEDPToken(); - - const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - - const response = await fetch( - `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - "TOOL_ID": "eVCP" - }) - } - ); - - if (!response.ok) { - throw new Error(`새 레지스터 요청 실패: ${response.status} ${response.statusText}`); - } - - // 안전하게 JSON 파싱 - let data; - try { - data = await response.json(); - } catch (parseError) { - console.error(`프로젝트 ${projectCode}의 새 레지스터 응답 파싱 실패:`, parseError); - // 응답 내용 로깅 - const text = await response.clone().text(); - console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); - throw new Error(`새 레지스터 응답 파싱 실패: ${parseError instanceof Error ? parseError.message : String(parseError)}`); - } - - // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑) - let registers: newRegister[] = Array.isArray(data) ? data : [data]; - - console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 새 레지스터를 가져왔습니다.`); - return registers; - } catch (error) { - console.error(`프로젝트 ${projectCode}의 새 레지스터 가져오기 실패:`, error); - throw error; - } -} - - -/** - * 태그 가져오기 서비스 함수 - * formEntries와 tags 테이블 모두에 데이터를 저장 - */ -export async function importTagsFromSEDP( - formCode: string, - projectCode: string, - packageCode: string, - progressCallback?: (progress: number) => void -): Promise<{ - processedCount: number; - excludedCount: number; - totalEntries: number; - formCreated?: boolean; - errors?: string[]; -}> { - try { - // 진행 상황 보고 - if (progressCallback) progressCallback(5); - - // 에러 수집 배열 - const errors: string[] = []; - - // SEDP API에서 태그 데이터 가져오기 - const tagData = await fetchTagDataFromSEDP(projectCode, formCode); - const newRegisters = await getNewRegisters(projectCode); - - const registerMatched = newRegisters.find(v => v.REG_TYPE_ID === formCode).MAP_ATT - - - // 트랜잭션으로 모든 DB 작업 처리 - return await db.transaction(async (tx) => { - // 프로젝트 정보 가져오기 (type 포함) - const projectRecord = await tx.select({ id: projects.id, type: projects.type }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectRecord || projectRecord.length === 0) { - throw new Error(`Project not found for code: ${projectCode}`); - } - - const projectId = projectRecord[0].id; - const projectType = projectRecord[0].type; - - // 프로젝트 타입에 따라 packageCode를 찾을 ATT_ID 결정 - const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074"; - - - - - const targetPackageCode = packageCode; - - // 데이터 형식 처리 - tagData의 첫 번째 키 사용 - const tableName = Object.keys(tagData)[0]; - - if (!tableName || !tagData[tableName]) { - throw new Error("Invalid tag data format from SEDP API"); - } - - const allTagEntries: TagEntry[] = tagData[tableName]; - - if (!Array.isArray(allTagEntries) || allTagEntries.length === 0) { - return { - processedCount: 0, - excludedCount: 0, - totalEntries: 0, - errors: ["No tag entries found in API response"] - }; - } - - // packageCode로 필터링 - ATTRIBUTES에서 지정된 ATT_ID의 VALUE와 packageCode 비교 - const tagEntries = allTagEntries.filter(entry => { - if (Array.isArray(entry.ATTRIBUTES)) { - const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId); - if (packageCodeAttr && packageCodeAttr.VALUE === targetPackageCode) { - return true; - } - } - return false; - }); - - if (tagEntries.length === 0) { - return { - processedCount: 0, - excludedCount: 0, - totalEntries: allTagEntries.length, - errors: [`No tag entries found with ${packageCodeAttId} attribute value matching packageCode: ${targetPackageCode}`] - }; - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(20); - - // 나머지 코드는 기존과 동일... - // form ID 가져오기 - 없으면 생성 - let formRecord = await tx.select({ id: formsPlant.id }) - .from(formsPlant) - .where(and( - eq(formsPlant.formCode, formCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode) - )) - .limit(1); - - let formCreated = false; - - // form이 없으면 생성 - if (!formRecord || formRecord.length === 0) { - console.log(`[IMPORT TAGS] Form ${formCode} not found, attempting to create...`); - - // 첫 번째 태그의 정보를 사용해서 form mapping을 찾습니다 - // 모든 태그가 같은 formCode를 사용한다고 가정 - if (tagEntries.length > 0) { - const firstTag = tagEntries[0]; - - // tagType 조회 (TAG_TYPE_ID -> description) - let tagTypeDescription = firstTag.TAG_TYPE_ID; // 기본값 - if (firstTag.TAG_TYPE_ID) { - const tagTypeRecord = await tx.select({ description: tagTypes.description }) - .from(tagTypes) - .where(and( - eq(tagTypes.code, firstTag.TAG_TYPE_ID), - eq(tagTypes.projectId, projectId) - )) - .limit(1); - - if (tagTypeRecord && tagTypeRecord.length > 0) { - tagTypeDescription = tagTypeRecord[0].description; - } - } - - // tagClass 조회 (CLS_ID -> label) - let tagClassLabel = firstTag.CLS_ID; // 기본값 - if (firstTag.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) - .from(tagClasses) - .where(and( - eq(tagClasses.code, firstTag.CLS_ID), - eq(tagClasses.projectId, projectId) - )) - .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { - tagClassLabel = tagClassRecord[0].label; - } - } - - // 태그 타입에 따른 폼 정보 가져오기 - const allFormMappings = await getFormMappingsByTagTypebyProeject( - projectId, - ); - - // 현재 formCode와 일치하는 매핑 찾기 - const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); - - if (targetFormMapping) { - console.log(`[IMPORT TAGS] Found form mapping for ${formCode}, creating form...`); - - // form 생성 - const insertResult = await tx - .insert(formsPlant) - .values({ - projectCode, - packageCode, - formCode: targetFormMapping.formCode, - formName: targetFormMapping.formName, - eng: true, // ENG 모드에서 가져오는 것이므로 eng: true - im: targetFormMapping.ep === "IMEP" ? true : false - }) - .returning({ id: formsPlant.id }); - - formRecord = insertResult; - formCreated = true; - - console.log(`[IMPORT TAGS] Successfully created form:`, insertResult[0]); - } else { - console.log(`[IMPORT TAGS] No form mapping found for formCode: ${formCode}`); - console.log(`[IMPORT TAGS] Available mappings:`, allFormMappings.map(m => m.formCode)); - throw new Error(`Form ${formCode} not found and no mapping available for tag type ${tagTypeDescription}`); - } - } else { - throw new Error(`Form not found for formCode: ${formCode} and, and no tags to derive form mapping`); - } - } else { - console.log(`[IMPORT TAGS] Found existing form:`, formRecord[0].id); - - // 기존 form이 있는 경우 eng와 im 필드를 체크하고 업데이트 - const existingForm = await tx.select({ - eng: formsPlant.eng, - im: formsPlant.im - }) - .from(formsPlant) - .where(eq(formsPlant.id, formRecord[0].id)) - .limit(1); - - if (existingForm.length > 0) { - // form mapping 정보 가져오기 (im 필드 업데이트를 위해) - let shouldUpdateIm = false; - let targetImValue = false; - - // 첫 번째 태그의 정보를 사용해서 form mapping을 확인 - if (tagEntries.length > 0) { - const firstTag = tagEntries[0]; - - // tagType 조회 - let tagTypeDescription = firstTag.TAG_TYPE_ID; - if (firstTag.TAG_TYPE_ID) { - const tagTypeRecord = await tx.select({ description: tagTypes.description }) - .from(tagTypes) - .where(and( - eq(tagTypes.code, firstTag.TAG_TYPE_ID), - eq(tagTypes.projectId, projectId) - )) - .limit(1); - - if (tagTypeRecord && tagTypeRecord.length > 0) { - tagTypeDescription = tagTypeRecord[0].description; - } - } - - // tagClass 조회 - let tagClassLabel = firstTag.CLS_ID; - if (firstTag.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) - .from(tagClasses) - .where(and( - eq(tagClasses.code, firstTag.CLS_ID), - eq(tagClasses.projectId, projectId) - )) - .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { - tagClassLabel = tagClassRecord[0].label; - } - } - - // form mapping 정보 가져오기 - const allFormMappings = await getFormMappingsByTagTypebyProeject( - projectId, - ); - - // 현재 formCode와 일치하는 매핑 찾기 - const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); - - if (targetFormMapping) { - targetImValue = targetFormMapping.ep === "IMEP"; - shouldUpdateIm = existingForm[0].im !== targetImValue; - } - } - - // 업데이트할 필드들 준비 - const updates: any = {}; - let hasUpdates = false; - - // eng 필드 체크 - if (existingForm[0].eng !== true) { - updates.eng = true; - hasUpdates = true; - } - - // im 필드 체크 - if (shouldUpdateIm) { - updates.im = targetImValue; - hasUpdates = true; - } - - // 업데이트 실행 - if (hasUpdates) { - await tx - .update(formsPlant) - .set(updates) - .where(eq(formsPlant.id, formRecord[0].id)); - - console.log(`[IMPORT TAGS] Form ${formRecord[0].id} updated with:`, updates); - } - } - } - - const formId = formRecord[0].id; - - // 나머지 처리 로직은 기존과 동일... - // (양식 메타데이터 가져오기, 태그 처리 등) - - // 양식 메타데이터 가져오기 - const formMetaRecord = await tx.select({ columns: formMetas.columns }) - .from(formMetas) - .where(and( - eq(formMetas.projectId, projectId), - eq(formMetas.formCode, formCode) - )) - .limit(1); - - if (!formMetaRecord || formMetaRecord.length === 0) { - throw new Error(`Form metadata not found for formCode: ${formCode} and projectId: ${projectId}`); - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(30); - - // 컬럼 정보 파싱 - const columnsJSON: Column[] = (formMetaRecord[0].columns); - - // 현재 formEntries 데이터 가져오기 - const existingEntries = await tx.select({ id: formEntriesPlant.id, data: formEntriesPlant.data }) - .from(formEntriesPlant) - .where(and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) - )); - - // 기존 tags 데이터 가져오기 - const existingTags = await tx.select() - .from(tagsPlant) - .where(and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode), - ) - ); - - // 진행 상황 보고 - if (progressCallback) progressCallback(50); - - // 기존 데이터를 맵으로 변환 - const existingTagMap = new Map(); - const existingTagsMap = new Map(); - - existingEntries.forEach(entry => { - const data = entry.data as any[]; - data.forEach(item => { - if (item.TAG_IDX) { - existingTagMap.set(item.TAG_IDX, { - entryId: entry.id, - data: item - }); - } - }); - }); - - existingTags.forEach(tag => { - existingTagsMap.set(tag.tagIdx, tag); - }); - - // 진행 상황 보고 - if (progressCallback) progressCallback(60); - - // 처리 결과 카운터 - let processedCount = 0; - let excludedCount = 0; - - // 새로운 태그 데이터와 업데이트할 데이터 준비 - const newTagData: any[] = []; - const upsertTagRecords: any[] = []; // 새로 추가되거나 업데이트될 태그들 - const updateData: { entryId: number, tagNo: string, updates: any }[] = []; - - // SEDP 태그 데이터 처리 - for (const tagEntry of tagEntries) { - try { - if (!tagEntry.TAG_IDX) { - excludedCount++; - errors.push(`Missing TAG_NO in tag entry`); - continue; - } - - // tagType 조회 (TAG_TYPE_ID -> description) - let tagTypeDescription = tagEntry.TAG_TYPE_ID; // 기본값 - if (tagEntry.TAG_TYPE_ID) { - const tagTypeRecord = await tx.select({ description: tagTypes.description }) - .from(tagTypes) - .where(and( - eq(tagTypes.code, tagEntry.TAG_TYPE_ID), - eq(tagTypes.projectId, projectId) - )) - .limit(1); - - if (tagTypeRecord && tagTypeRecord.length > 0) { - tagTypeDescription = tagTypeRecord[0].description; - } - } - - // tagClass 조회 (CLS_ID -> label) - let tagClassLabel = tagEntry.CLS_ID; // 기본값 - let tagClassId = null; // 기본값 - if (tagEntry.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) - .from(tagClasses) - .where(and( - eq(tagClasses.code, tagEntry.CLS_ID), - eq(tagClasses.projectId, projectId) - )) - .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { - tagClassLabel = tagClassRecord[0].label; - tagClassId = tagClassRecord[0].id; - } - } - - const packageCode = projectType === "ship" ? tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "CM3003")?.VALUE : tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "ME5074")?.VALUE - - // 기본 태그 데이터 객체 생성 (formEntries용) - const tagObject: any = { - TAG_IDX: tagEntry.TAG_IDX, // SEDP 고유 식별자 - TAG_NO: tagEntry.TAG_NO, - TAG_DESC: tagEntry.TAG_DESC || "", - CLS_ID: tagEntry.CLS_ID || "", - VNDRCD: vendorRecord[0].vendorCode, - VNDRNM_1: vendorRecord[0].vendorName, - status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시 - source: "S-EDP", // 태그 출처 (불변) - S-EDP에서 가져옴 - ...(projectType === "ship" ? { CM3003: packageCode } : { ME5074: packageCode }) - } - - let latestDueDate: Date | null = null; - - // tags 테이블용 데이터 (UPSERT용) - const tagRecord = { - projectCode, - packageCode, - formId: formId, - tagIdx: tagEntry.TAG_IDX, // SEDP 고유 식별자 - tagNo: tagEntry.TAG_NO, - tagType: tagTypeDescription || "", - class: tagClassLabel, - tagClassId: tagClassId, - description: tagEntry.TAG_DESC || null, - createdAt: new Date(), - updatedAt: new Date() - }; - - // ATTRIBUTES 필드에서 shi=true인 컬럼의 값 추출 - if (Array.isArray(tagEntry.ATTRIBUTES)) { - for (const attr of tagEntry.ATTRIBUTES) { - const columnInfo = columnsJSON.find(col => col.key === attr.ATT_ID); - if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) { - if (columnInfo.type === "NUMBER") { - if (attr.VALUE !== undefined && attr.VALUE !== null) { - if (typeof attr.VALUE === 'string') { - const numberMatch = attr.VALUE.match(/(-?\d+(\.\d+)?)/); - if (numberMatch) { - tagObject[attr.ATT_ID] = parseFloat(numberMatch[0]); - } else { - const parsed = parseFloat(attr.VALUE); - if (!isNaN(parsed)) { - tagObject[attr.ATT_ID] = parsed; - } - } - } else if (typeof attr.VALUE === 'number') { - tagObject[attr.ATT_ID] = attr.VALUE; - } - } - } else if (attr.VALUE !== null && attr.VALUE !== undefined) { - tagObject[attr.ATT_ID] = attr.VALUE; - } - } - - // registerMatched에서 해당 SEDP_ATT_ID의 DUE_DATE 찾기 - if (registerMatched && Array.isArray(registerMatched)) { - const matchedAttribute = registerMatched.find( - regAttr => regAttr.SEDP_ATT_ID === attr.ATT_ID - ); - - if (matchedAttribute && matchedAttribute.DUE_DATE) { - try { - const dueDate = new Date(matchedAttribute.DUE_DATE); - - // 유효한 날짜인지 확인 - if (!isNaN(dueDate.getTime())) { - // 첫 번째 DUE_DATE이거나 현재까지 찾은 것보다 더 늦은 날짜인 경우 업데이트 - if (!latestDueDate || dueDate > latestDueDate) { - latestDueDate = dueDate; - } - } - } catch (dateError) { - console.warn(`Invalid DUE_DATE format for ${attr.ATT_ID}: ${matchedAttribute.DUE_DATE}`); - } - } - } - - } - } - - if (latestDueDate) { - // ISO 형식의 문자열로 저장 (또는 원하는 형식으로 변경 가능) - tagObject.DUE_DATE = latestDueDate.toISOString(); - - // 또는 YYYY-MM-DD 형식을 원한다면: - // tagObject.DUE_DATE = latestDueDate.toISOString().split('T')[0]; - - // 또는 원본 형식 그대로 유지하려면: - // tagObject.DUE_DATE = latestDueDate.toISOString().replace('Z', ''); - } - - - - // 기존 태그가 있는지 확인하고 처리 - const existingTag = existingTagMap.get(tagEntry.TAG_IDX); - - if (existingTag) { - // 기존 태그가 있으면 formEntries 업데이트 데이터 준비 - const updates: any = {}; - let hasUpdates = false; - - for (const key of Object.keys(tagObject)) { - if (key === "TAG_IDX") continue; - - if (key === "TAG_NO" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - - if (key === "TAG_DESC" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - if (key === "status" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - if (key === "CLS_ID" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - if (key === "DUE_DATE" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - const columnInfo = columnsJSON.find(col => col.key === key); - if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) { - if (existingTag.data[key] !== tagObject[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - } - } - } - - if (hasUpdates) { - updateData.push({ - entryId: existingTag.entryId, - tagIdx: tagEntry.TAG_IDX, // TAG_IDX로 변경 - updates - }); - } - } else { - // 기존 태그가 없으면 새로 추가 - newTagData.push(tagObject); - } - - // tags 테이블에는 항상 upsert (새로 추가되거나 업데이트) - upsertTagRecords.push(tagRecord); - - processedCount++; - } catch (error) { - excludedCount++; - errors.push(`Error processing tag ${tagEntry.TAG_IDX || 'unknown'}: ${error}`); - } - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(80); - - // formEntries 업데이트 실행 - // entryId별로 업데이트를 그룹화 - const updatesByEntryId = new Map(); - - for (const update of updateData) { - if (!updatesByEntryId.has(update.entryId)) { - updatesByEntryId.set(update.entryId, []); - } - updatesByEntryId.get(update.entryId).push(update); - } - - // 그룹화된 업데이트를 처리 - for (const [entryId, updates] of updatesByEntryId) { - try { - const entry = existingEntries.find(e => e.id === entryId); - if (!entry) continue; - - const data = entry.data as any[]; - - // 해당 entryId의 모든 업데이트를 한 번에 적용 - const updatedData = data.map(item => { - let updatedItem = { ...item }; - - // 현재 item에 적용할 모든 업데이트를 찾아서 적용 - for (const update of updates) { - if (item.TAG_IDX === update.tagIdx) { - updatedItem = { ...updatedItem, ...update.updates }; - } - } - - return updatedItem; - }); - - // entryId별로 한 번만 DB 업데이트 - await tx.update(formEntriesPlant) - .set({ - data: updatedData, - updatedAt: new Date() - }) - .where(eq(formEntriesPlant.id, entryId)); - - } catch (error) { - const tagNos = updates.map(u => u.tagNo || u.tagIdx).join(', '); - errors.push(`Error updating formEntry ${entryId} for tags ${tagNos}: ${error}`); - } - } - - // 새 태그 추가 (formEntriesPlant) - if (newTagData.length > 0) { - if (existingEntries.length > 0) { - const firstEntry = existingEntries[0]; - const existingData = firstEntry.data as any[]; - const updatedData = [...existingData, ...newTagData]; - - await tx.update(formEntriesPlant) - .set({ - data: updatedData, - updatedAt: new Date() - }) - .where(eq(formEntriesPlant.id, firstEntry.id)); - } else { - await tx.insert(formEntriesPlant) - .values({ - formCode, - projectCode, - packageCode, - data: newTagData, - createdAt: new Date(), - updatedAt: new Date() - }); - } - } - - // tags 테이블 처리 (INSERT + UPDATE 분리) - if (upsertTagRecords.length > 0) { - const newTagRecords: any[] = []; - const updateTagRecords: { tagId: number, updates: any }[] = []; - - // 각 태그를 확인하여 신규/업데이트 분류 - for (const tagRecord of upsertTagRecords) { - const existingTagRecord = existingTagsMap.get(tagRecord.tagIdx); - - if (existingTagRecord) { - // 기존 태그가 있으면 업데이트 준비 - const tagUpdates: any = {}; - let hasTagUpdates = false; - - // tagNo도 업데이트 가능 (편집된 경우) - if (existingTagRecord.tagNo !== tagRecord.tagNo) { - tagUpdates.tagNo = tagRecord.tagNo; - hasTagUpdates = true; - } - - if (existingTagRecord.tagType !== tagRecord.tagType) { - tagUpdates.tagType = tagRecord.tagType; - hasTagUpdates = true; - } - if (existingTagRecord.class !== tagRecord.class) { - tagUpdates.class = tagRecord.class; - hasTagUpdates = true; - } - if (existingTagRecord.tagClassId !== tagRecord.tagClassId) { - tagUpdates.tagClassId = tagRecord.tagClassId; - hasTagUpdates = true; - } - - if (existingTagRecord.description !== tagRecord.description) { - tagUpdates.description = tagRecord.description; - hasTagUpdates = true; - } - if (existingTagRecord.formId !== tagRecord.formId) { - tagUpdates.formId = tagRecord.formId; - hasTagUpdates = true; - } - - if (hasTagUpdates) { - updateTagRecords.push({ - tagId: existingTagRecord.id, - updates: { ...tagUpdates, updatedAt: new Date() } - }); - } - } else { - // 새로운 태그 - newTagRecords.push(tagRecord); - } - } - - // 새 태그 삽입 - if (newTagRecords.length > 0) { - try { - await tx.insert(tagsPlant) - .values(newTagRecords) - .onConflictDoNothing({ - target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx] - }); - } catch (error) { - // 개별 삽입으로 재시도 - for (const tagRecord of newTagRecords) { - try { - await tx.insert(tagsPlant) - .values(tagRecord) - .onConflictDoNothing({ - target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx] - }); - } catch (individualError) { - errors.push(`Error inserting tag ${tagRecord.tagIdx}: ${individualError}`); - } - } - } - } - - // 기존 태그 업데이트 - for (const update of updateTagRecords) { - try { - await tx.update(tagsPlant) - .set(update.updates) - .where(eq(tagsPlant.id, update.tagId)); - } catch (error) { - errors.push(`Error updating tag record ${update.tagId}: ${error}`); - } - } - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(100); - - // 최종 결과 반환 - return { - processedCount, - excludedCount, - totalEntries: tagEntries.length, - formCreated, - errors: errors.length > 0 ? errors : undefined - }; - }); - - } catch (error: any) { - console.error("Tag import error:", error); - throw error; - } -} -/** - * SEDP API에서 태그 데이터 가져오기 - * - * @param projectCode 프로젝트 코드 - * @param formCode 양식 코드 - * @returns API 응답 데이터 - */ -async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> { - try { - // Get the token - const apiKey = await getSEDPToken(); - - // Define the API base URL - const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - - // Make the API call - const response = await fetch( - `${SEDP_API_BASE_URL}/Data/GetPubData`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - REG_TYPE_ID: formCode, - // TODO: 이창국 프로 요청으로, ContainDeleted: true로 변경예정, EDP에서 삭제된 데이터도 가져올 수 있어야 한다고 함. - // 삭제된 게 들어오면 eVCP내에서 지우거나, 비활성화 하는 등의 처리를 해야 할 걸로 보임 - ContainDeleted: false - }) - } - ); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); - } - - const data = await response.json(); - return data; - } catch (error: any) { - console.error('Error calling SEDP API:', error); - throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`); - } -}
\ No newline at end of file diff --git a/lib/sedp/get-tags-plant.ts b/lib/sedp/get-tags-plant.ts deleted file mode 100644 index d1957db4..00000000 --- a/lib/sedp/get-tags-plant.ts +++ /dev/null @@ -1,639 +0,0 @@ -import db from "@/db/db"; -import { - tagsPlant, - formsPlant, - formEntriesPlant, - items, - tagTypeClassFormMappings, - projects, - tagTypes, - tagClasses, -} from "@/db/schema"; -import { eq, and, like, inArray } from "drizzle-orm"; -import { revalidateTag } from "next/cache"; // 추가 -import { getSEDPToken } from "./sedp-token"; - -/** - * 태그 가져오기 서비스 함수 - * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장 - * TAG_IDX를 기준으로 태그를 식별합니다. - * - * @param projectCode 계약 아이템 ID (contractItemId) - * @param packageCode 계약 아이템 ID (contractItemId) - * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수 - * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등) - */ -export async function importTagsFromSEDP( - projectCode: string, - packageCode: string, - progressCallback?: (progress: number) => void, - mode?: string -): Promise<{ - processedCount: number; - excludedCount: number; - totalEntries: number; - errors?: string[]; -}> { - try { - // 진행 상황 보고 - if (progressCallback) progressCallback(5); - - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); - - - // 프로젝트 ID 획득 - const projectId = project?.id; - - // Step 1-2: Get the item using itemId from contractItem - const item = await db.query.items.findFirst({ - where: and(eq(items.ProjectNo, projectCode), eq(items.packageCode, packageCode)) - }); - - if (!item) { - throw new Error(`Item with ID ${item?.id} not found`); - } - - const itemCode = item.itemCode; - - // 진행 상황 보고 - if (progressCallback) progressCallback(10); - - // 기본 매핑 검색 - 모든 모드에서 사용 - const baseMappings = await db.query.tagTypeClassFormMappings.findMany({ - where: and( - like(tagTypeClassFormMappings.remark, `%${itemCode}%`), - eq(tagTypeClassFormMappings.projectId, projectId) - ) - }); - - if (baseMappings.length === 0) { - throw new Error(`No mapping found for item code ${itemCode}`); - } - - // Step 2: Find the mapping entries - 모드에 따라 다른 조건 적용 - let mappings = []; - - if (mode === 'IM') { - // IM 모드일 때는 먼저 SEDP에서 태그 데이터를 가져와 TAG_TYPE_ID 리스트 확보 - - // 프로젝트 코드 가져오기 - const project = await db.query.projects.findFirst({ - where: eq(projects.id, projectId) - }); - - if (!project) { - throw new Error(`Project with ID ${projectId} not found`); - } - - // 각 매핑의 formCode에 대해 태그 데이터 조회 - const tagTypeIds = new Set<string>(); - - for (const mapping of baseMappings) { - try { - // SEDP에서 태그 데이터 가져오기 - const tagData = await fetchTagDataFromSEDP(project.code, mapping.formCode); - - // 첫 번째 키를 테이블 이름으로 사용 - const tableName = Object.keys(tagData)[0]; - const tagEntries = tagData[tableName]; - - if (Array.isArray(tagEntries)) { - // 모든 태그에서 TAG_TYPE_ID 수집 - for (const entry of tagEntries) { - if (entry.TAG_TYPE_ID && entry.TAG_TYPE_ID !== "") { - tagTypeIds.add(entry.TAG_TYPE_ID); - } - } - } - } catch (error) { - console.error(`Error fetching tag data for formCode ${mapping.formCode}:`, error); - } - } - - if (tagTypeIds.size === 0) { - throw new Error('No valid TAG_TYPE_ID found in SEDP tag data'); - } - - // 수집된 TAG_TYPE_ID로 tagTypes에서 정보 조회 - const tagTypeInfo = await db.query.tagTypes.findMany({ - where: and( - inArray(tagTypes.code, Array.from(tagTypeIds)), - eq(tagTypes.projectId, projectId) - ) - }); - - if (tagTypeInfo.length === 0) { - throw new Error('No matching tag types found for the collected TAG_TYPE_IDs'); - } - - // 태그 타입 설명 수집 - const tagLabels = tagTypeInfo.map(tt => tt.description); - - // IM 모드에 맞는 매핑 조회 - ep가 "IMEP"인 항목만 - mappings = await db.query.tagTypeClassFormMappings.findMany({ - where: and( - inArray(tagTypeClassFormMappings.tagTypeLabel, tagLabels), - eq(tagTypeClassFormMappings.projectId, projectId), - eq(tagTypeClassFormMappings.ep, "IMEP") - ) - }); - - } else { - // ENG 모드 또는 기본 모드일 때 - 기본 매핑 사용 - mappings = [...baseMappings]; - - // ENG 모드에서는 ep 필드가 "IMEP"가 아닌 매핑만 필터링 - if (mode === 'ENG') { - mappings = mappings.filter(mapping => mapping.ep !== "IMEP"); - } - } - - // 매핑이 없는 경우 모드에 따라 다른 오류 메시지 사용 - if (mappings.length === 0) { - if (mode === 'IM') { - throw new Error('No suitable mappings found for IM mode'); - } else { - throw new Error(`No mapping found for item code ${itemCode}`); - } - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(15); - - // 결과 누적을 위한 변수들 초기화 - let totalProcessedCount = 0; - let totalExcludedCount = 0; - let totalEntriesCount = 0; - const allErrors: string[] = []; - - // 각 매핑에 대해 처리 - for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) { - const mapping = mappings[mappingIndex]; - - // Step 3: Get the project code - const project = await db.query.projects.findFirst({ - where: eq(projects.id, mapping.projectId) - }); - - if (!project) { - allErrors.push(`Project with ID ${mapping.projectId} not found`); - continue; // 다음 매핑으로 진행 - } - - // IM 모드에서는 baseMappings에서 같은 formCode를 가진 매핑을 찾음 - let formCode = mapping.formCode; - if (mode === 'IM') { - // baseMapping에서 동일한 formCode를 가진 매핑 찾기 - const originalMapping = baseMappings.find( - baseMapping => baseMapping.formCode === mapping.formCode - ); - - // 찾았으면 해당 formCode 사용, 못 찾았으면 현재 매핑의 formCode 유지 - if (originalMapping) { - formCode = originalMapping.formCode; - } - } - - // 진행 상황 보고 - 매핑별 진행률 조정 - if (progressCallback) { - const baseProgress = 15; - const mappingProgress = Math.floor(15 * (mappingIndex + 1) / mappings.length); - progressCallback(baseProgress + mappingProgress); - } - - // Step 4: Find the form ID - const form = await db.query.formsPlant.findFirst({ - where: and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formCode) - ) - }); - - let formId; - - // If form doesn't exist, create it - if (!form) { - // 폼이 없는 경우 새로 생성 - 모드에 따른 필드 설정 - const insertValues: any = { - projectCode, - packageCode, - formCode: formCode, - formName: mapping.formName - }; - - // 모드 정보가 있으면 해당 필드 설정 - if (mode) { - if (mode === "ENG") { - insertValues.eng = true; - } else if (mode === "IM") { - insertValues.im = true; - if (mapping.remark && mapping.remark.includes("VD_")) { - insertValues.eng = true; - } - } - } - - const insertResult = await db.insert(formsPlant).values(insertValues).returning({ id: formsPlant.id }); - - if (insertResult.length === 0) { - allErrors.push(`Failed to create form record for formCode ${formCode}`); - continue; // 다음 매핑으로 진행 - } - - formId = insertResult[0].id; - } else { - // 폼이 이미 존재하는 경우 - 필요시 모드 필드 업데이트 - formId = form.id; - - if (mode) { - let shouldUpdate = false; - const updateValues: any = {}; - - if (mode === "ENG" && form.eng !== true) { - updateValues.eng = true; - shouldUpdate = true; - } else if (mode === "IM" && form.im !== true) { - updateValues.im = true; - shouldUpdate = true; - } - - if (shouldUpdate) { - await db.update(formsPlant) - .set({ - ...updateValues, - updatedAt: new Date() - }) - .where(eq(formsPlant.id, formId)); - - console.log(`Updated form ${formId} with ${mode} mode enabled`); - } - } - } - - // 진행 상황 보고 - 매핑별 진행률 조정 - if (progressCallback) { - const baseProgress = 30; - const mappingProgress = Math.floor(20 * (mappingIndex + 1) / mappings.length); - progressCallback(baseProgress + mappingProgress); - } - - try { - // Step 5: Call the external API to get tag data - const tagData = await fetchTagDataFromSEDP(projectCode, baseMappings[0].formCode); - - // 진행 상황 보고 - if (progressCallback) { - const baseProgress = 50; - const mappingProgress = Math.floor(10 * (mappingIndex + 1) / mappings.length); - progressCallback(baseProgress + mappingProgress); - } - - // Step 6: Process the data and insert into the tags table - let processedCount = 0; - let excludedCount = 0; - - // Get the first key from the response as the table name - const tableName = Object.keys(tagData)[0]; - const tagEntries = tagData[tableName]; - - if (!Array.isArray(tagEntries) || tagEntries.length === 0) { - allErrors.push(`No tag data found in the API response for formCode ${baseMappings[0].formCode}`); - continue; // 다음 매핑으로 진행 - } - - const entriesCount = tagEntries.length; - totalEntriesCount += entriesCount; - - // formEntries를 위한 데이터 수집 - const newTagsForFormEntry: Array<{ - TAG_IDX: string; // 변경: TAG_NO → TAG_IDX - TAG_NO?: string; // TAG_NO도 함께 저장 (편집 가능한 필드) - TAG_DESC: string | null; - status: string; - [key: string]: any; - }> = []; - - const registerResponse = await fetch( - `${SEDP_API_BASE_URL}/Register/GetByID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TYPE_ID: baseMappings[0].formCode, // 또는 mapping.formCode - ContainDeleted: false - }) - } - ) - - if (!registerResponse.ok) { - allErrors.push(`Failed to fetch register details for ${baseMappings[0].formCode}`) - continue - } - - const registerDetail: Register = await registerResponse.json() - - // ✅ MAP_ATT에서 허용된 ATT_ID 목록 추출 - const allowedAttIds = new Set<string>() - if (Array.isArray(registerDetail.MAP_ATT)) { - for (const mapAttr of registerDetail.MAP_ATT) { - if (mapAttr.ATT_ID) { - allowedAttIds.add(mapAttr.ATT_ID) - } - } - } - - - // Process each tag entry - for (let i = 0; i < tagEntries.length; i++) { - try { - const entry = tagEntries[i]; - - // TAG_IDX가 없는 경우 제외 (변경: TAG_NO → TAG_IDX 체크) - if (!entry.TAG_IDX) { - excludedCount++; - totalExcludedCount++; - - // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트) - if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { - const baseProgress = 60; - const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount))); - progressCallback(baseProgress + entryProgress); - } - - continue; // 이 항목은 건너뜀 - } - - const attributes: Record<string, string> = {} - if (Array.isArray(entry.ATTRIBUTES)) { - for (const attr of entry.ATTRIBUTES) { - // MAP_ATT에 정의된 ATT_ID만 포함 - if (attr.ATT_ID && allowedAttIds.has(attr.ATT_ID)) { - if (attr.VALUE !== null && attr.VALUE !== undefined) { - attributes[attr.ATT_ID] = String(attr.VALUE) - } - } - } - } - - - // TAG_TYPE_ID가 null이거나 빈 문자열인 경우 제외 - if (entry.TAG_TYPE_ID === null || entry.TAG_TYPE_ID === "") { - excludedCount++; - totalExcludedCount++; - - // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트) - if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { - const baseProgress = 60; - const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount))); - progressCallback(baseProgress + entryProgress); - } - - continue; // 이 항목은 건너뜀 - } - - // Get tag type description - const tagType = await db.query.tagTypes.findFirst({ - where: and( - eq(tagTypes.code, entry.TAG_TYPE_ID), - eq(tagTypes.projectId, mapping.projectId) - ) - }); - - // Get tag class label - const tagClass = await db.query.tagClasses.findFirst({ - where: and( - eq(tagClasses.code, entry.CLS_ID), - eq(tagClasses.projectId, mapping.projectId) - ) - }); - - // Insert or update the tag - tagIdx 필드 추가 - await db.insert(tagsPlant).values({ - projectCode, - packageCode, - formId: formId, - tagIdx: entry.TAG_IDX, - tagNo: entry.TAG_NO || entry.TAG_IDX, - tagType: tagType?.description || entry.TAG_TYPE_ID, - tagClassId: tagClass?.id, - class: tagClass?.label || entry.CLS_ID, - description: entry.TAG_DESC, - attributes: attributes, // JSONB로 저장 - }).onConflictDoUpdate({ - target: [tagsPlant.projectCode, tagsPlant.packageCode, tagsPlant.tagIdx], - set: { - formId: formId, - tagNo: entry.TAG_NO || entry.TAG_IDX, - tagType: tagType?.description || entry.TAG_TYPE_ID, - class: tagClass?.label || entry.CLS_ID, - description: entry.TAG_DESC, - attributes: attributes, // JSONB 업데이트 - updatedAt: new Date() - } - }) - // formEntries용 데이터 수집 - const tagDataForFormEntry = { - TAG_IDX: entry.TAG_IDX, // 변경: TAG_NO → TAG_IDX - TAG_NO: entry.TAG_NO || entry.TAG_IDX, // TAG_NO도 함께 저장 - TAG_DESC: entry.TAG_DESC || null, - status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시 - source: "S-EDP" // 태그 출처 (불변) - S-EDP에서 가져옴 - }; - - // ATTRIBUTES가 있으면 추가 (SHI 필드들) - if (Array.isArray(entry.ATTRIBUTES)) { - for (const attr of entry.ATTRIBUTES) { - if (attr.ATT_ID && attr.VALUE !== null && attr.VALUE !== undefined) { - tagDataForFormEntry[attr.ATT_ID] = attr.VALUE; - } - } - } - - newTagsForFormEntry.push(tagDataForFormEntry); - - processedCount++; - totalProcessedCount++; - - // 주기적으로 진행 상황 보고 - if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { - const baseProgress = 60; - const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount))); - progressCallback(baseProgress + entryProgress); - } - } catch (error: any) { - console.error(`Error processing tag entry:`, error); - allErrors.push(error.message || 'Unknown error'); - } - } - - // Step 7: formEntries 업데이트 - TAG_IDX 기준으로 변경 - if (newTagsForFormEntry.length > 0) { - try { - // 기존 formEntry 가져오기 - const existingEntry = await db.query.formEntriesPlant.findFirst({ - where: and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) - ) - }); - - if (existingEntry && existingEntry.id) { - // 기존 formEntry가 있는 경우 - let existingData: Array<{ - TAG_IDX?: string; // 추가: TAG_IDX 필드 - TAG_NO?: string; - TAG_DESC?: string; - status?: string; - [key: string]: any; - }> = []; - - if (Array.isArray(existingEntry.data)) { - existingData = existingEntry.data; - } - - // 기존 TAG_IDX들 추출 (변경: TAG_NO → TAG_IDX) - const existingTagIdxs = new Set( - existingData - .map(item => item.TAG_IDX) - .filter(tagIdx => tagIdx !== undefined && tagIdx !== null) - ); - - // 중복되지 않은 새 태그들만 필터링 (변경: TAG_NO → TAG_IDX) - const newUniqueTagsData = newTagsForFormEntry.filter( - tagData => !existingTagIdxs.has(tagData.TAG_IDX) - ); - - // 기존 태그들의 status와 ATTRIBUTES 업데이트 (변경: TAG_NO → TAG_IDX) - const updatedExistingData = existingData.map(existingItem => { - const newTagData = newTagsForFormEntry.find( - newItem => newItem.TAG_IDX === existingItem.TAG_IDX - ); - - if (newTagData) { - // 기존 태그가 있으면 SEDP 데이터로 업데이트 - return { - ...existingItem, - ...newTagData, - TAG_IDX: existingItem.TAG_IDX // TAG_IDX는 유지 - }; - } - - return existingItem; - }); - - const finalData = [...updatedExistingData, ...newUniqueTagsData]; - - await db - .update(formEntriesPlant) - .set({ - data: finalData, - updatedAt: new Date() - }) - .where(eq(formEntriesPlant.id, existingEntry.id)); - - console.log(`[IMPORT SEDP] Updated formEntry with ${newUniqueTagsData.length} new tags, updated ${updatedExistingData.length - newUniqueTagsData.length} existing tags for form ${formCode}`); - } else { - // formEntry가 없는 경우 새로 생성 - await db.insert(formEntriesPlant).values({ - formCode: formCode, - projectCode, - packageCode, - data: newTagsForFormEntry, - createdAt: new Date(), - updatedAt: new Date(), - }); - - console.log(`[IMPORT SEDP] Created new formEntry with ${newTagsForFormEntry.length} tags for form ${formCode}`); - } - - // 캐시 무효화 - revalidateTag(`form-data-${formCode}-${packageId}`); - } catch (formEntryError) { - console.error(`[IMPORT SEDP] Error updating formEntry for form ${formCode}:`, formEntryError); - allErrors.push(`Error updating formEntry for form ${formCode}: ${formEntryError}`); - } - } - - } catch (error: any) { - console.error(`Error processing mapping for formCode ${formCode}:`, error); - allErrors.push(`Error with formCode ${formCode}: ${error.message || 'Unknown error'}`); - } - } - - // 모든 매핑 처리 완료 - 진행률 100% - if (progressCallback) { - progressCallback(100); - } - - // 최종 결과 반환 - return { - processedCount: totalProcessedCount, - excludedCount: totalExcludedCount, - totalEntries: totalEntriesCount, - errors: allErrors.length > 0 ? allErrors : undefined - }; - } catch (error: any) { - console.error("Tag import error:", error); - throw error; - } -} - -/** - * SEDP API에서 태그 데이터 가져오기 - * - * @param projectCode 프로젝트 코드 - * @param formCode 양식 코드 - * @returns API 응답 데이터 - */ -async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> { - try { - // Get the token - const apiKey = await getSEDPToken(); - - // Define the API base URL - const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - - // Make the API call - const response = await fetch( - `${SEDP_API_BASE_URL}/Data/GetPubData`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - REG_TYPE_ID: formCode, - ContainDeleted: false - }) - } - ); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); - } - - const data = await response.json(); - return data; - } catch (error: any) { - console.error('Error calling SEDP API:', error); - throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`); - } -}
\ No newline at end of file diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts index a6d473ad..904d27ba 100644 --- a/lib/sedp/sync-form.ts +++ b/lib/sedp/sync-form.ts @@ -94,7 +94,7 @@ interface Register { SEQ: number; CMPLX_YN: boolean; CMPL_SETT: any | null; - MAP_ATT: MapAttribute2[]; + MAP_ATT: any[]; MAP_CLS_ID: string[]; MAP_OPER: any | null; LNK_ATT: LinkAttribute[]; @@ -157,13 +157,6 @@ interface MapAttribute { INOUT: string | null; } -interface MapAttribute2 { - ATT_ID: string; - VALUE: string; - IS_PARA: boolean; - OPER: string | null; -} - interface Attribute { PROJ_NO: string; ATT_ID: string; diff --git a/lib/tags-plant/column-builder.service.ts b/lib/tags-plant/column-builder.service.ts deleted file mode 100644 index 9a552d6e..00000000 --- a/lib/tags-plant/column-builder.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -// lib/vendor-data-plant/column-builder.service.ts -import { ColumnDef } from "@tanstack/react-table" -import { Tag } from "@/db/schema/vendorData" - -/** - * 동적 속성 컬럼 생성 (ATT_ID만 사용, 라벨 없음) - */ -export function createDynamicAttributeColumns( - attributeKeys: string[] -): ColumnDef<Tag>[] { - return attributeKeys.map(key => ({ - id: `attr_${key}`, - accessorFn: (row: Tag) => { - if (row.attributes && typeof row.attributes === 'object') { - return (row.attributes as Record<string, string>)[key] || '' - } - return '' - }, - header: key, // 단순 문자열로 반환 - cell: ({ getValue }) => { - const value = getValue() - return value as string || "-" - }, - meta: { - excelHeader: key - }, - enableSorting: true, - enableColumnFilter: true, - filterFn: "includesString", - enableResizing: true, - minSize: 100, - size: 150, - })) -}
\ No newline at end of file diff --git a/lib/tags-plant/queries.ts b/lib/tags-plant/queries.ts deleted file mode 100644 index a0d28b1e..00000000 --- a/lib/tags-plant/queries.ts +++ /dev/null @@ -1,68 +0,0 @@ -// lib/vendor-data-plant/queries.ts -"use server" - -import db from "@/db/db" - -import { tagsPlant } from "@/db/schema/vendorData" -import { eq, and } from "drizzle-orm" - -/** - * 모든 태그 가져오기 (클라이언트 렌더링용) - */ -export async function getAllTagsPlant( - projectCode: string, - packageCode: string -) { - try { - const tags = await db - .select() - .from(tagsPlant) - .where( - and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode) - ) - ) - .orderBy(tagsPlant.createdAt) - - return tags - } catch (error) { - console.error("Error fetching all tags:", error) - return [] - } -} - -/** - * 고유 속성 키 추출 - */ -export async function getUniqueAttributeKeys( - projectCode: string, - packageCode: string -): Promise<string[]> { - try { - const result = await db - .select({ - attributes: tagsPlant.attributes - }) - .from(tagsPlant) - .where( - and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode) - ) - ) - - const allKeys = new Set<string>() - - for (const row of result) { - if (row.attributes && typeof row.attributes === 'object') { - Object.keys(row.attributes).forEach(key => allKeys.add(key)) - } - } - - return Array.from(allKeys).sort() - } catch (error) { - console.error("Error getting unique attribute keys:", error) - return [] - } -}
\ No newline at end of file diff --git a/lib/tags-plant/repository.ts b/lib/tags-plant/repository.ts index bbe36f66..b5d48335 100644 --- a/lib/tags-plant/repository.ts +++ b/lib/tags-plant/repository.ts @@ -1,5 +1,5 @@ import db from "@/db/db"; -import { NewTag, tags, tagsPlant } from "@/db/schema/vendorData"; +import { NewTag, tags } from "@/db/schema/vendorData"; import { eq, inArray, @@ -69,43 +69,3 @@ export async function deleteTagsByIds( ) { return tx.delete(tags).where(inArray(tags.id, ids)); } - - -export async function selectTagsPlant( - tx: PgTransaction<any, any, any>, - params: { - where?: any; // drizzle-orm의 조건식 (and, eq...) 등 - orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[]; - offset?: number; - limit?: number; - } -) { - const { where, orderBy, offset = 0, limit = 10 } = params; - - return tx - .select() - .from(tagsPlant) - .where(where) - .orderBy(...(orderBy ?? [])) - .offset(offset) - .limit(limit); -} -/** 총 개수 count */ -export async function countTagsPlant( - tx: PgTransaction<any, any, any>, - where?: any -) { - const res = await tx.select({ count: count() }).from(tagsPlant).where(where); - return res[0]?.count ?? 0; -} - -export async function insertTagPlant( - tx: PgTransaction<any, any, any>, - data: NewTag // DB와 동일한 insert 가능한 타입 - ) { - // returning() 사용 시 배열로 돌아오므로 [0]만 리턴 - return tx - .insert(tagsPlant) - .values(data) - .returning({ id: tagsPlant.id, createdAt: tagsPlant.createdAt }); - }
\ No newline at end of file diff --git a/lib/tags-plant/service.ts b/lib/tags-plant/service.ts index 02bd33be..778ab89d 100644 --- a/lib/tags-plant/service.ts +++ b/lib/tags-plant/service.ts @@ -1,14 +1,14 @@ "use server" import db from "@/db/db" -import { formEntries, forms,items,formsPlant, tagClasses, tags, tagsPlant, tagSubfieldOptions, tagSubfields, tagTypes,formEntriesPlant } from "@/db/schema" +import { formEntries, forms, tagClasses, tags, tagSubfieldOptions, tagSubfields, tagTypes } from "@/db/schema/vendorData" // import { eq } from "drizzle-orm" import { createTagSchema, GetTagsSchema, updateTagSchema, UpdateTagSchema, type CreateTagSchema } from "./validations" import { revalidateTag, unstable_noStore } from "next/cache"; import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, sql, ne, count, isNull } from "drizzle-orm"; -import { countTags, insertTag, selectTags, selectTagsPlant, countTagsPlant,insertTagPlant } from "./repository"; +import { countTags, insertTag, selectTags } from "./repository"; import { getErrorMessage } from "../handle-error"; import { getFormMappingsByTagType } from './form-mapping-service'; import { contractItems, contracts } from "@/db/schema/contract"; @@ -32,8 +32,7 @@ function generateTagIdx(): string { return randomBytes(12).toString('hex'); // 12바이트 = 24자리 16진수 } - -export async function getTagsPlant(input: GetTagsSchema, projectCode: string,packageCode: string ) { +export async function getTags(input: GetTagsSchema, packagesId: number) { // return unstable_cache( // async () => { @@ -42,7 +41,7 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac // (1) advancedWhere const advancedWhere = filterColumns({ - table: tagsPlant, + table: tags, filters: input.filters, joinOperator: input.joinOperator, }); @@ -52,31 +51,31 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac if (input.search) { const s = `%${input.search}%`; globalWhere = or( - ilike(tagsPlant.tagNo, s), - ilike(tagsPlant.tagType, s), - ilike(tagsPlant.description, s) + ilike(tags.tagNo, s), + ilike(tags.tagType, s), + ilike(tags.description, s) ); } - // (4) 최종 projectCode - const finalWhere = and(advancedWhere, globalWhere, eq(tagsPlant.projectCode, projectCode), eq(tagsPlant.packageCode, packageCode)); + // (4) 최종 where + const finalWhere = and(advancedWhere, globalWhere, eq(tags.contractItemId, packagesId)); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => - item.desc ? desc(tagsPlant[item.id]) : asc(tagsPlant[item.id]) + item.desc ? desc(tags[item.id]) : asc(tags[item.id]) ) - : [asc(tagsPlant.createdAt)]; + : [asc(tags.createdAt)]; // 트랜잭션 내부에서 Repository 호출 const { data, total } = await db.transaction(async (tx) => { - const data = await selectTagsPlant(tx, { + const data = await selectTags(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); - const total = await countTagsPlant(tx, finalWhere); + const total = await countTags(tx, finalWhere); return { data, total }; @@ -102,10 +101,9 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac export async function createTag( formData: CreateTagSchema, - projectCode: string, - packageCode: string, + selectedPackageId: number | null ) { - if (!projectCode) { + if (!selectedPackageId) { return { error: "No selectedPackageId provided" } } @@ -121,23 +119,33 @@ export async function createTag( try { // 하나의 트랜잭션에서 모든 작업 수행 return await db.transaction(async (tx) => { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 1) 선택된 contractItem의 contractId 가져오기 + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1) - const projectId = project.id + if (contractItemResult.length === 0) { + return { error: "Contract item not found" } + } + + const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인 const duplicateCheck = await tx .select({ count: sql<number>`count(*)` }) - .from(tagsPlant) + .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.tagNo, validated.data.tagNo) + eq(contractItems.contractId, contractId), + eq(tags.tagNo, validated.data.tagNo) ) ) @@ -174,16 +182,16 @@ export async function createTag( const createdOrExistingForms: CreatedOrExistingForm[] = [] if (formMappings && formMappings.length > 0) { + console.log(selectedPackageId, formMappings) for (const formMapping of formMappings) { // 4-1) 이미 존재하는 폼인지 확인 const existingForm = await tx - .select({ id: formsPlant.id, im: formsPlant.im, eng: formsPlant.eng }) // eng 필드도 추가로 조회 - .from(formsPlant) + .select({ id: forms.id, im: forms.im, eng: forms.eng }) // eng 필드도 추가로 조회 + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1) @@ -211,9 +219,9 @@ export async function createTag( if (shouldUpdate) { await tx - .update(formsPlant) + .update(forms) .set(updateValues) - .where(eq(formsPlant.id, formId)) + .where(eq(forms.id, formId)) console.log(`Form ${formId} updated with:`, updateValues) } @@ -227,8 +235,7 @@ export async function createTag( } else { // 존재하지 않으면 새로 생성 const insertValues: any = { - projectCode: projectCode, - packageCode: packageCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, im: true, @@ -240,9 +247,9 @@ export async function createTag( } const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values(insertValues) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }) console.log("insertResult:", insertResult) formId = insertResult[0].id @@ -266,9 +273,8 @@ export async function createTag( console.log(`[CREATE TAG] Generated tagIdx: ${generatedTagIdx}`); // 5) 새 Tag 생성 (tagIdx 추가) - const [newTag] = await insertTagPlant(tx, { - packageCode:packageCode, - projectCode:projectCode, + const [newTag] = await insertTag(tx, { + contractItemId: selectedPackageId, formId: primaryFormId, tagIdx: generatedTagIdx, // 🆕 생성된 16진수 24자리 추가 tagNo: validated.data.tagNo, @@ -277,6 +283,7 @@ export async function createTag( description: validated.data.description ?? null, }) + console.log(`tags-${selectedPackageId}`, "create", newTag) // 6) 생성된 각 form에 대해 formEntries에 데이터 추가 (TAG_IDX 포함) for (const form of createdOrExistingForms) { @@ -284,9 +291,8 @@ export async function createTag( // 기존 formEntry 가져오기 const existingEntry = await tx.query.formEntries.findFirst({ where: and( - eq(formEntriesPlant.formCode, form.formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, form.formCode), + eq(formEntries.contractItemId, selectedPackageId) ) }); @@ -323,12 +329,12 @@ export async function createTag( const updatedData = [...existingData, newTagData]; await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, existingEntry.id)); + .where(eq(formEntries.id, existingEntry.id)); console.log(`[CREATE TAG] Added tag ${validated.data.tagNo} with tagIdx ${generatedTagIdx} to existing formEntry for form ${form.formCode}`); } else { @@ -336,10 +342,9 @@ export async function createTag( } } else { // formEntry가 없는 경우 새로 생성 (TAG_IDX 포함) - await tx.insert(formEntriesPlant).values({ + await tx.insert(formEntries).values({ formCode: form.formCode, - projectCode: projectCode, - packageCode: packageCode, + contractItemId: selectedPackageId, data: [newTagData], createdAt: new Date(), updatedAt: new Date(), @@ -353,6 +358,16 @@ export async function createTag( } } + // 7) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시) + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}-ENG`) + revalidateTag("tags") + + // 생성된 각 form의 캐시도 무효화 + createdOrExistingForms.forEach(form => { + revalidateTag(`form-data-${form.formCode}-${selectedPackageId}`) + }) + // 8) 성공 시 반환 (tagIdx 추가) return { success: true, @@ -651,11 +666,10 @@ export async function createTagInForm( export async function updateTag( formData: UpdateTagSchema & { id: number }, - projectCode: string, - packageCode: string, + selectedPackageId: number | null ) { - if (!projectCode) { - return { error: "No projectCode provided" } + if (!selectedPackageId) { + return { error: "No selectedPackageId provided" } } if (!formData.id) { @@ -687,25 +701,35 @@ export async function updateTag( const originalTag = existingTag[0] - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 2) 선택된 contractItem의 contractId 가져오기 + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1) + + if (contractItemResult.length === 0) { + return { error: "Contract item not found" } + } - const projectId = project.id + const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 3) 태그 번호가 변경되었고, 해당 계약 내에서 같은 tagNo를 가진 다른 태그가 있는지 확인 if (originalTag.tagNo !== validated.data.tagNo) { const duplicateCheck = await tx .select({ count: sql<number>`count(*)` }) - .from(tagsPlant) + .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.tagNo, validated.data.tagNo), - ne(tagsPlant.id, formData.id) // 자기 자신은 제외 + eq(contractItems.contractId, contractId), + eq(tags.tagNo, validated.data.tagNo), + ne(tags.id, formData.id) // 자기 자신은 제외 ) ) @@ -750,12 +774,11 @@ export async function updateTag( // 이미 존재하는 폼인지 확인 const existingForm = await tx .select({ id: forms.id }) - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1) @@ -773,14 +796,13 @@ export async function updateTag( } else { // 존재하지 않으면 새로 생성 const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values({ - projectCode, - packageCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, }) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }) formId = insertResult[0].id createdOrExistingForms.push({ @@ -801,10 +823,9 @@ export async function updateTag( // 5) 태그 업데이트 const [updatedTag] = await tx - .update(tagsPlant) + .update(tags) .set({ - projectCode, - packageCode, + contractItemId: selectedPackageId, formId: primaryFormId, tagNo: validated.data.tagNo, class: validated.data.class, @@ -812,9 +833,12 @@ export async function updateTag( description: validated.data.description ?? null, updatedAt: new Date(), }) - .where(eq(tagsPlant.id, formData.id)) + .where(eq(tags.id, formData.id)) .returning() + // 6) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시) + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}`) revalidateTag("tags") // 7) 성공 시 반환 @@ -843,8 +867,7 @@ export interface TagInputData { // 새로운 서버 액션 export async function bulkCreateTags( tagsfromExcel: TagInputData[], - projectCode: string, - packageCode: string + selectedPackageId: number ) { unstable_noStore(); @@ -856,22 +879,31 @@ export async function bulkCreateTags( // 단일 트랜잭션으로 모든 작업 처리 return await db.transaction(async (tx) => { // 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만) - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (contractItemResult.length === 0) { + return { error: "Contract item not found" }; + } - const projectId = project.id + const contractId = contractItemResult[0].contractId; + const projectId = contractItemResult[0].projectId; // projectId 추출 // 2. 모든 태그 번호 중복 검사 (한 번에) const tagNos = tagsfromExcel.map(tag => tag.tagNo); const duplicateCheck = await tx .select({ tagNo: tags.tagNo }) .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where(and( - eq(tags.projectCode, projectCode), + eq(contractItems.contractId, contractId), inArray(tags.tagNo, tagNos) )); @@ -937,13 +969,12 @@ export async function bulkCreateTags( for (const formMapping of formMappings) { // 해당 폼이 이미 존재하는지 확인 const existingForm = await tx - .select({ id: formsPlant.id, im: formsPlant.im }) - .from(formsPlant) + .select({ id: forms.id, im: forms.im }) + .from(forms) .where( and( - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1); @@ -956,9 +987,9 @@ export async function bulkCreateTags( // im 필드 업데이트 (필요한 경우) if (existingForm[0].im !== true) { await tx - .update(formsPlant) + .update(forms) .set({ im: true }) - .where(eq(formsPlant.id, formId)); + .where(eq(forms.id, formId)); } createdOrExistingForms.push({ @@ -970,15 +1001,14 @@ export async function bulkCreateTags( } else { // 존재하지 않으면 새로 생성 const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values({ - packageCode:packageCode, - projectCode:projectCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, im: true }) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }); + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }); formId = insertResult[0].id; createdOrExistingForms.push({ @@ -1018,9 +1048,8 @@ export async function bulkCreateTags( } // 태그 생성 - const [newTag] = await insertTagPlant(tx, { - packageCode:packageCode, - projectCode:projectCode, + const [newTag] = await insertTag(tx, { + contractItemId: selectedPackageId, formId: primaryFormId, tagNo: tagData.tagNo, class: tagData.class || "", @@ -1038,15 +1067,14 @@ export async function bulkCreateTags( }); } - // 4. formEntriesPlant 업데이트 처리 + // 4. formEntries 업데이트 처리 for (const [formCode, newTagsData] of tagsByFormCode.entries()) { try { // 기존 formEntry 가져오기 - const existingEntry = await tx.query.formEntriesPlant.findFirst({ + const existingEntry = await tx.query.formEntries.findFirst({ where: and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, selectedPackageId) ) }); @@ -1075,12 +1103,12 @@ export async function bulkCreateTags( const updatedData = [...existingData, ...newUniqueTagsData]; await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, existingEntry.id)); + .where(eq(formEntries.id, existingEntry.id)); console.log(`[BULK CREATE] Added ${newUniqueTagsData.length} tags to existing formEntry for form ${formCode}`); } else { @@ -1088,10 +1116,9 @@ export async function bulkCreateTags( } } else { // formEntry가 없는 경우 새로 생성 - await tx.insert(formEntriesPlant).values({ + await tx.insert(formEntries).values({ formCode: formCode, - projectCode:projectCode, - packageCode:packageCode, + contractItemId: selectedPackageId, data: newTagsData, createdAt: new Date(), updatedAt: new Date(), @@ -1105,6 +1132,16 @@ export async function bulkCreateTags( } } + // 5. 캐시 무효화 (한 번만) + revalidateTag(`tags-${selectedPackageId}`); + revalidateTag(`forms-${selectedPackageId}`); + revalidateTag("tags"); + + // 업데이트된 모든 form의 캐시도 무효화 + for (const formCode of tagsByFormCode.keys()) { + revalidateTag(`form-data-${formCode}-${selectedPackageId}`); + } + return { success: true, data: { @@ -1123,8 +1160,7 @@ export async function bulkCreateTags( /** 복수 삭제 */ interface RemoveTagsInput { ids: number[]; - projectCode: string; - packageCode: string; + selectedPackageId: number; } @@ -1142,29 +1178,36 @@ function removeTagFromDataJson( export async function removeTags(input: RemoveTagsInput) { unstable_noStore() // React 서버 액션 무상태 함수 - const { ids, projectCode, packageCode } = input + const { ids, selectedPackageId } = input try { await db.transaction(async (tx) => { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); - const projectId = project.id; + const packageInfo = await tx + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } + + const projectId = packageInfo[0].projectId; // 1) 삭제 대상 tag들을 미리 조회 const tagsToDelete = await tx .select({ - id: tagsPlant.id, - tagNo: tagsPlant.tagNo, - tagType: tagsPlant.tagType, - class: tagsPlant.class, + id: tags.id, + tagNo: tags.tagNo, + tagType: tags.tagType, + class: tags.class, }) - .from(tagsPlant) - .where(inArray(tagsPlant.id, ids)) + .from(tags) + .where(inArray(tags.id, ids)) // 2) 태그 타입과 클래스의 고유 조합 추출 const uniqueTypeClassCombinations = [...new Set( @@ -1179,14 +1222,13 @@ export async function removeTags(input: RemoveTagsInput) { // 3-1) 삭제 중인 태그들 외에, 동일한 태그 타입/클래스를 가진 다른 태그가 있는지 확인 const otherTagsWithSameTypeClass = await tx .select({ count: count() }) - .from(tagsPlant) + .from(tags) .where( and( - eq(tagsPlant.tagType, tagType), - classValue ? eq(tagsPlant.class, classValue) : isNull(tagsPlant.class), - not(inArray(tagsPlant.id, ids)), - eq(tags.packageCode, packageCode), - eq(tags.projectCode, projectCode) // 같은 contractItemId 내에서만 확인 + eq(tags.tagType, tagType), + classValue ? eq(tags.class, classValue) : isNull(tags.class), + not(inArray(tags.id, ids)), // 현재 삭제 중인 태그들은 제외 + eq(tags.contractItemId, selectedPackageId) // 같은 contractItemId 내에서만 확인 ) ) @@ -1207,23 +1249,21 @@ export async function removeTags(input: RemoveTagsInput) { if (otherTagsWithSameTypeClass[0].count === 0) { // 폼 삭제 await tx - .delete(formsPlant) + .delete(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) // formEntries 테이블에서도 해당 formCode 관련 데이터 삭제 await tx - .delete(formEntriesPlant) + .delete(formEntries) .where( and( - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.formCode, formMapping.formCode) + eq(formEntries.contractItemId, selectedPackageId), + eq(formEntries.formCode, formMapping.formCode) ) ) } @@ -1231,15 +1271,14 @@ export async function removeTags(input: RemoveTagsInput) { else if (relevantTagNos.length > 0) { const formEntryRecords = await tx .select({ - id: formEntriesPlant.id, - data: formEntriesPlant.data, + id: formEntries.id, + data: formEntries.data, }) - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.formCode, formMapping.formCode) + eq(formEntries.contractItemId, selectedPackageId), + eq(formEntries.formCode, formMapping.formCode) ) ) @@ -1266,6 +1305,9 @@ export async function removeTags(input: RemoveTagsInput) { await tx.delete(tags).where(inArray(tags.id, ids)) }) + // 5) 캐시 무효화 + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}`) return { data: null, error: null } } catch (err) { @@ -1286,26 +1328,25 @@ export interface ClassOption { * Class 옵션 목록을 가져오는 함수 * 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함 */ -export async function getClassOptions( - packageCode: string, - projectCode: string -): Promise<UpdatedClassOption[]> { +export async function getClassOptions(selectedPackageId: number): Promise<UpdatedClassOption[]> { try { - // 1. 프로젝트 정보 조회 - const projectInfo = await db - .select() - .from(projects) - .where(eq(projects.code, projectCode)) + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) .limit(1); - if (projectInfo.length === 0) { - throw new Error(`Project with code ${projectCode} not found`); + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); } - const projectId = projectInfo[0].id; - + const projectId = packageInfo[0].projectId; - // 3. 태그 클래스들을 서브클래스 정보와 함께 조회 + // 2. 태그 클래스들을 서브클래스 정보와 함께 조회 const tagClassesWithSubclasses = await db .select({ id: tagClasses.id, @@ -1319,8 +1360,8 @@ export async function getClassOptions( .where(eq(tagClasses.projectId, projectId)) .orderBy(tagClasses.code); - // 4. 태그 타입 정보도 함께 조회 (description을 위해) - const tagTypesMap = new Map<string, string>(); + // 3. 태그 타입 정보도 함께 조회 (description을 위해) + const tagTypesMap = new Map(); const tagTypesList = await db .select({ code: tagTypes.code, @@ -1329,24 +1370,21 @@ export async function getClassOptions( .from(tagTypes) .where(eq(tagTypes.projectId, projectId)); - tagTypesList.forEach((tagType) => { + tagTypesList.forEach(tagType => { tagTypesMap.set(tagType.code, tagType.description); }); - // 5. 클래스 옵션으로 변환 - const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map( - (cls) => ({ - value: cls.code, - label: cls.label, - code: cls.code, - description: cls.label, - tagTypeCode: cls.tagTypeCode, - tagTypeDescription: - tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, - subclasses: cls.subclasses || [], - subclassRemark: cls.subclassRemark || {}, - }) - ); + // 4. 클래스 옵션으로 변환 + const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map(cls => ({ + value: cls.code, + label: cls.label, + code: cls.code, + description: cls.label, + tagTypeCode: cls.tagTypeCode, + tagTypeDescription: tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, + subclasses: cls.subclasses || [], + subclassRemark: cls.subclassRemark || {}, + })); return classOptions; } catch (error) { @@ -1354,8 +1392,6 @@ export async function getClassOptions( throw new Error("Failed to fetch class options"); } } - - interface SubFieldDef { name: string label: string @@ -1367,20 +1403,26 @@ interface SubFieldDef { export async function getSubfieldsByTagType( tagTypeCode: string, - projectCode: string, + selectedPackageId: number, subclassRemark: string = "", subclass: string = "", ) { try { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } - const projectId = project.id + const projectId = packageInfo[0].projectId; // 2. 올바른 projectId를 사용하여 tagSubfields 조회 const rows = await db @@ -1581,314 +1623,29 @@ export interface TagTypeOption { label: string; // tagTypes.description 값 } -export async function getProjectIdFromContractItemId( - projectCode: string -): Promise<number | null> { +export async function getProjectIdFromContractItemId(contractItemId: number): Promise<number | null> { try { // First get the contractId from contractItems - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), + const contractItem = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, contractItemId), columns: { - id: true + contractId: true } }); - if (!project) return null; - - return project?.id || null; - } catch (error) { - console.error("Error fetching projectId:", error); - return null; - } -} - -const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - + if (!contractItem) return null; -/** - * Engineering 폼 목록 가져오기 - */ -export async function getEngineeringForms( - projectCode: string, - packageCode: string -): Promise<FormInfo[]> { - try { - // 1. DB에서 eng=true인 폼 조회 - const existingForms = await db - .select({ - formCode: formsPlant.formCode, - formName: formsPlant.formName, - }) - .from(formsPlant) - .where( - and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.eng, true) - ) - ) - - // DB에 데이터가 있으면 반환 - if (existingForms.length > 0) { - return existingForms - } - - // 2. DB에 없으면 SEDP API에서 가져오기 - const apiKey = await getSEDPToken() - - // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기 - const mappingResponse = await fetch( - `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TOOL_ID: "eVCP" - }) - } - ) - - if (!mappingResponse.ok) { - throw new Error( - `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}` - ) - } - - const mappingData = await mappingResponse.json() - const registers: NewRegister[] = Array.isArray(mappingData) - ? mappingData - : [mappingData] - - // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링 - const matchingRegisters = registers.filter(register => - register.SCOPES.includes(packageCode) - ) - - if (matchingRegisters.length === 0) { - console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`) - return [] - } - - // 2-3. 각 레지스터의 상세 정보 가져오기 - const formInfos: FormInfo[] = [] - const formsToInsert: typeof formsPlant.$inferInsert[] = [] - - for (const register of matchingRegisters) { - try { - const detailResponse = await fetch( - `${SEDP_API_BASE_URL}/Register/GetByID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TYPE_ID: register.REG_TYPE_ID, - ContainDeleted: false - }) - } - ) - - if (!detailResponse.ok) { - console.error( - `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}` - ) - continue - } - - const detail: RegisterDetail = await detailResponse.json() - - // DELETED가 true이거나 DESC가 없으면 스킵 - if (detail.DELETED || !detail.DESC) { - continue - } - - formInfos.push({ - formCode: detail.TYPE_ID, - formName: detail.DESC - }) - - // DB 삽입용 데이터 준비 - formsToInsert.push({ - projectCode: projectCode, - packageCode: packageCode, - formCode: detail.TYPE_ID, - formName: detail.DESC, - eng: true, - im: false - }) - } catch (error) { - console.error( - `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`, - error - ) - continue - } - } - - // 2-4. DB에 저장 - if (formsToInsert.length > 0) { - await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing() - console.log(`${formsToInsert.length}개의 Engineering 폼을 DB에 저장했습니다.`) - } - - return formInfos - } catch (error) { - console.error("Engineering 폼 가져오기 실패:", error) - throw new Error("Failed to fetch engineering forms") - } -} - -/** - * IM 폼 목록 가져오기 - */ -export async function getIMForms( - projectCode: string, - packageCode: string -): Promise<FormInfo[]> { - try { - // 1. DB에서 im=true인 폼 조회 - const existingForms = await db - .select({ - formCode: formsPlant.formCode, - formName: formsPlant.formName, - }) - .from(formsPlant) - .where( - and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.im, true) - ) - ) - - // DB에 데이터가 있으면 반환 - if (existingForms.length > 0) { - return existingForms - } - - // 2. DB에 없으면 SEDP API에서 가져오기 - const apiKey = await getSEDPToken() - - // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기 - const mappingResponse = await fetch( - `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TOOL_ID: "eVCP" - }) - } - ) - - if (!mappingResponse.ok) { - throw new Error( - `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}` - ) - } - - const mappingData = await mappingResponse.json() - const registers: NewRegister[] = Array.isArray(mappingData) - ? mappingData - : [mappingData] - - // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링 - const matchingRegisters = registers.filter(register => - register.SCOPES.includes(packageCode) - ) - - if (matchingRegisters.length === 0) { - console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`) - return [] - } - - // 2-3. 각 레지스터의 상세 정보 가져오기 - const formInfos: FormInfo[] = [] - const formsToInsert: typeof formsPlant.$inferInsert[] = [] - - for (const register of matchingRegisters) { - try { - const detailResponse = await fetch( - `${SEDP_API_BASE_URL}/Register/GetByID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TYPE_ID: register.REG_TYPE_ID, - ContainDeleted: false - }) - } - ) - - if (!detailResponse.ok) { - console.error( - `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}` - ) - continue - } - - const detail: RegisterDetail = await detailResponse.json() - - // DELETED가 true이거나 DESC가 없으면 스킵 - if (detail.DELETED || !detail.DESC) { - continue - } - - formInfos.push({ - formCode: detail.TYPE_ID, - formName: detail.DESC - }) - - // DB 삽입용 데이터 준비 - formsToInsert.push({ - projectCode: projectCode, - packageCode: packageCode, - formCode: detail.TYPE_ID, - formName: detail.DESC, - eng: false, - im: true - }) - } catch (error) { - console.error( - `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`, - error - ) - continue + // Then get the projectId from contracts + const contract = await db.query.contracts.findFirst({ + where: eq(contracts.id, contractItem.contractId), + columns: { + projectId: true } - } - - // 2-4. DB에 저장 - if (formsToInsert.length > 0) { - await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing() - console.log(`${formsToInsert.length}개의 IM 폼을 DB에 저장했습니다.`) - } + }); - return formInfos + return contract?.projectId || null; } catch (error) { - console.error("IM 폼 가져오기 실패:", error) - throw new Error("Failed to fetch IM forms") + console.error("Error fetching projectId:", error); + return null; } }
\ No newline at end of file diff --git a/lib/tags-plant/table/add-tag-dialog.tsx b/lib/tags-plant/table/add-tag-dialog.tsx index 41731f63..9c82bf1a 100644 --- a/lib/tags-plant/table/add-tag-dialog.tsx +++ b/lib/tags-plant/table/add-tag-dialog.tsx @@ -61,7 +61,7 @@ import { getClassOptions, type ClassOption, TagTypeOption, -} from "@/lib/tags-plant/service" +} from "@/lib/tags/service" import { ScrollArea } from "@/components/ui/scroll-area" // Updated to support multiple rows and subclass @@ -98,11 +98,10 @@ interface UpdatedClassOption extends ClassOption { } interface AddTagDialogProps { - projectCode: string - packageCode: string + selectedPackageId: number } -export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { +export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const router = useRouter() const params = useParams() const lng = (params?.lng as string) || "ko" @@ -126,6 +125,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { const fieldIdsRef = React.useRef<Record<string, string>>({}) const classOptionIdsRef = React.useRef<Record<string, string>>({}) + console.log(selectedPackageId, "tag") // --------------- // Load Class Options (서브클래스 정보 포함) @@ -135,7 +135,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { setIsLoadingClasses(true) try { // getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정 - const result = await getClassOptions(packageCode, projectCode) + const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { toast.error(t("toast.classOptionsLoadFailed")) @@ -147,7 +147,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { if (open) { loadClassOptions() } - }, [open, projectCode, packageCode]) + }, [open, selectedPackageId]) // --------------- // react-hook-form with fieldArray support for multiple rows @@ -176,7 +176,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { setIsLoadingSubFields(true) try { // 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가) - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, subclassRemark, subclass) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId, subclassRemark, subclass) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -313,7 +313,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { // Submit handler for multiple tags (서브클래스 정보 포함) // --------------- async function onSubmit(data: MultiTagFormValues) { - if (!projectCode) { + if (!selectedPackageId) { toast.error(t("toast.noSelectedPackageId")); return; } @@ -343,7 +343,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { }; try { - const res = await createTag(tagData, projectCode, packageCode); + const res = await createTag(tagData, selectedPackageId); if ("error" in res) { console.log(res.error) failedTags.push({ tag: row.tagNo, error: res.error }); diff --git a/lib/tags-plant/table/delete-tags-dialog.tsx b/lib/tags-plant/table/delete-tags-dialog.tsx index 69a4f4a6..6a024cda 100644 --- a/lib/tags-plant/table/delete-tags-dialog.tsx +++ b/lib/tags-plant/table/delete-tags-dialog.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { type Row } from "@tanstack/react-table" import { Loader, Trash } from "lucide-react" import { toast } from "sonner" + import { useMediaQuery } from "@/hooks/use-media-query" import { Button } from "@/components/ui/button" import { @@ -26,15 +27,15 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" -import { removeTags } from "@/lib//tags-plant/service" + +import { removeTags } from "@/lib//tags/service" import { Tag } from "@/db/schema/vendorData" interface DeleteTasksDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { tags: Row<Tag>["original"][] showTrigger?: boolean - projectCode: string - packageCode: string + selectedPackageId: number onSuccess?: () => void } @@ -42,8 +43,7 @@ export function DeleteTagsDialog({ tags, showTrigger = true, onSuccess, - projectCode, - packageCode, + selectedPackageId, ...props }: DeleteTasksDialogProps) { const [isDeletePending, startDeleteTransition] = React.useTransition() @@ -52,7 +52,7 @@ export function DeleteTagsDialog({ function onDelete() { startDeleteTransition(async () => { const { error } = await removeTags({ - ids: tags.map((tag) => tag.id),projectCode, packageCode + ids: tags.map((tag) => tag.id),selectedPackageId }) if (error) { diff --git a/lib/tags-plant/table/tag-table.tsx b/lib/tags-plant/table/tag-table.tsx index 2fdcd5fc..1986d933 100644 --- a/lib/tags-plant/table/tag-table.tsx +++ b/lib/tags-plant/table/tag-table.tsx @@ -1,4 +1,3 @@ -// components/vendor-data-plant/tags-table.tsx "use client" import * as React from "react" @@ -7,177 +6,40 @@ import type { DataTableFilterField, DataTableRowAction, } from "@/types/table" -import { useRouter } from "next/navigation" -import { toast } from "sonner" -import { Trash2, Download, Upload, Loader2, RefreshCcw, Plus } from "lucide-react" -import ExcelJS from "exceljs" -import type { Table as TanstackTable } from "@tanstack/react-table" -import { ClientDataTable } from "@/components/client-data-table/data-table" +import { toSentenceCase } from "@/lib/utils" +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" + import { getColumns } from "./tag-table-column" import { Tag } from "@/db/schema/vendorData" import { DeleteTagsDialog } from "./delete-tags-dialog" +import { TagsTableToolbarActions } from "./tags-table-toolbar-actions" +import { TagsTableFloatingBar } from "./tags-table-floating-bar" +import { getTags } from "../service" import { UpdateTagSheet } from "./update-tag-sheet" -import { AddTagDialog } from "./add-tag-dialog" import { useAtomValue } from 'jotai' import { selectedModeAtom } from '@/atoms' -import { Skeleton } from "@/components/ui/skeleton" -import type { ColumnDef } from "@tanstack/react-table" -import { createDynamicAttributeColumns } from "../column-builder.service" -import { getAllTagsPlant, getUniqueAttributeKeys } from "../queries" -import { Button } from "@/components/ui/button" -import { exportTagsToExcel } from "./tags-export" -import { - bulkCreateTags, - getClassOptions, - getProjectIdFromContractItemId, - getSubfieldsByTagType -} from "../service" -import { decryptWithServerAction } from "@/components/drm/drmUtils" +// 여기서 받은 `promises`로부터 태그 목록을 가져와 상태를 세팅 +// 예: "selectedPackageId"는 props로 전달 interface TagsTableProps { - projectCode: string - packageCode: string -} - -// 태그 넘버링 룰 인터페이스 (Import용) -interface TagNumberingRule { - attributesId: string; - attributesDescription: string; - expression: string | null; - delimiter: string | null; - sortOrder: number; -} - -interface ClassOption { - code: string; - label: string; - tagTypeCode: string; - tagTypeDescription: string; -} - -interface SubFieldDef { - name: string; - label: string; - type: "select" | "text"; - options?: { value: string; label: string }[]; - expression?: string; - delimiter?: string; + promises: Promise< [ Awaited<ReturnType<typeof getTags>> ] > + selectedPackageId: number } -export function TagsTable({ - projectCode, - packageCode, -}: TagsTableProps) { - const router = useRouter() +export function TagsTable({ promises, selectedPackageId }: TagsTableProps) { + // 1) 데이터를 가져옴 (server component -> use(...) pattern) + const [{ data, pageCount }] = React.use(promises) const selectedMode = useAtomValue(selectedModeAtom) - // 상태 관리 - const [tableData, setTableData] = React.useState<Tag[]>([]) - const [columns, setColumns] = React.useState<ColumnDef<Tag>[]>([]) - const [isLoading, setIsLoading] = React.useState(true) const [rowAction, setRowAction] = React.useState<DataTableRowAction<Tag> | null>(null) - - // 선택된 행 관리 - const [selectedRowsData, setSelectedRowsData] = React.useState<Tag[]>([]) - const [clearSelection, setClearSelection] = React.useState(false) - - // 다이얼로그 상태 - const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) - const [deleteTarget, setDeleteTarget] = React.useState<Tag[]>([]) - const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false) - - // Import/Export 상태 - const [isPending, setIsPending] = React.useState(false) - const [isExporting, setIsExporting] = React.useState(false) - const fileInputRef = React.useRef<HTMLInputElement>(null) - - // Sync 상태 - const [isSyncing, setIsSyncing] = React.useState(false) - const [syncId, setSyncId] = React.useState<string | null>(null) - const pollingRef = React.useRef<NodeJS.Timeout | null>(null) - - // Table ref for export - const tableRef = React.useRef<TanstackTable<Tag> | null>(null) - - // Cache for validation - const [classOptions, setClassOptions] = React.useState<ClassOption[]>([]) - const [subfieldCache, setSubfieldCache] = React.useState<Record<string, SubFieldDef[]>>({}) - const [projectId, setProjectId] = React.useState<number | null>(null) - // Load project ID - React.useEffect(() => { - const fetchProjectId = async () => { - if (packageCode && projectCode) { - try { - const pid = await getProjectIdFromContractItemId(projectCode) - setProjectId(pid) - } catch (error) { - console.error("Failed to fetch project ID:", error) - } - } - } - fetchProjectId() - }, [projectCode]) - - // Load class options - React.useEffect(() => { - const loadClassOptions = async () => { - try { - const options = await getClassOptions(packageCode, projectCode) - setClassOptions(options) - } catch (error) { - console.error("Failed to load class options:", error) - } - } - loadClassOptions() - }, [packageCode, projectCode]) - - // 데이터 및 컬럼 로드 - React.useEffect(() => { - async function loadTableData() { - try { - setIsLoading(true) - - const [tagsData, attributeKeys] = await Promise.all([ - getAllTagsPlant(projectCode, packageCode), - getUniqueAttributeKeys(projectCode, packageCode), - ]) - - const baseColumns = getColumns({ - setRowAction, - onDeleteClick: handleDeleteRow - }) - - let dynamicColumns: ColumnDef<Tag>[] = [] - if (attributeKeys.length > 0) { - dynamicColumns = createDynamicAttributeColumns(attributeKeys) - } - - const actionsColumn = baseColumns.pop() - const finalColumns = [ - ...baseColumns, - ...dynamicColumns, - actionsColumn - ].filter(Boolean) as ColumnDef<Tag>[] - - setTableData(tagsData) - setColumns(finalColumns) - } catch (error) { - console.error("Error loading table data:", error) - toast.error("Failed to load table data") - setTableData([]) - setColumns(getColumns({ - setRowAction, - onDeleteClick: handleDeleteRow - })) - } finally { - setIsLoading(false) - } - } - - loadTableData() - }, [projectCode, packageCode]) + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) // Filter fields const filterFields: DataTableFilterField<Tag>[] = [ @@ -205,11 +67,6 @@ export function TagsTable({ type: "text", }, { - id: "class", - label: "Class", - type: "text", - }, - { id: "createdAt", label: "Created at", type: "date", @@ -221,562 +78,78 @@ export function TagsTable({ }, ] - // 선택된 행 개수 - const selectedRowCount = React.useMemo(() => { - return selectedRowsData.length - }, [selectedRowsData]) - - // 개별 행 삭제 - const handleDeleteRow = React.useCallback((rowData: Tag) => { - setDeleteTarget([rowData]) - setDeleteDialogOpen(true) - }, []) - - // 배치 삭제 - const handleBatchDelete = React.useCallback(() => { - if (selectedRowsData.length === 0) { - toast.error("삭제할 항목을 선택해주세요.") - return - } - setDeleteTarget(selectedRowsData) - setDeleteDialogOpen(true) - }, [selectedRowsData]) - - // 삭제 성공 후 처리 - const handleDeleteSuccess = React.useCallback(() => { - const tagNosToDelete = deleteTarget - .map(item => item.tagNo) - .filter(Boolean) - - setTableData(prev => - prev.filter(item => !tagNosToDelete.includes(item.tagNo)) - ) - - setSelectedRowsData([]) - setClearSelection(prev => !prev) - setDeleteTarget([]) - - toast.success("삭제되었습니다.") - }, [deleteTarget]) - - // 클래스 라벨로 태그 타입 코드 찾기 - const getTagTypeCodeByClassLabel = React.useCallback((classLabel: string): string | null => { - const classOption = classOptions.find(opt => opt.label === classLabel) - return classOption?.tagTypeCode || null - }, [classOptions]) - - // 태그 타입에 따른 서브필드 가져오기 - const fetchSubfieldsByTagType = React.useCallback(async (tagTypeCode: string): Promise<SubFieldDef[]> => { - if (subfieldCache[tagTypeCode]) { - return subfieldCache[tagTypeCode] - } - - try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, "", "") - const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ - name: field.name, - label: field.label, - type: field.type, - options: field.options || [], - expression: field.expression ?? undefined, - delimiter: field.delimiter ?? undefined, - })) - - setSubfieldCache(prev => ({ - ...prev, - [tagTypeCode]: formattedSubFields - })) - - return formattedSubFields - } catch (error) { - console.error(`Error fetching subfields for tagType ${tagTypeCode}:`, error) - return [] - } - }, [subfieldCache, projectCode]) - - // Class 기반 태그 번호 형식 검증 - const validateTagNumberByClass = React.useCallback(async ( - tagNo: string, - classLabel: string - ): Promise<string> => { - if (!tagNo) return "Tag number is empty." - if (!classLabel) return "Class is empty." - - try { - const tagTypeCode = getTagTypeCodeByClassLabel(classLabel) - if (!tagTypeCode) { - return `No tag type found for class '${classLabel}'.` - } - - const subfields = await fetchSubfieldsByTagType(tagTypeCode) - if (!subfields || subfields.length === 0) { - return `No subfields found for tag type code '${tagTypeCode}'.` - } - - let remainingTagNo = tagNo - - for (const field of subfields) { - const delimiter = field.delimiter || "" - let nextDelimiterPos - - if (delimiter && remainingTagNo.includes(delimiter)) { - nextDelimiterPos = remainingTagNo.indexOf(delimiter) - } else { - nextDelimiterPos = remainingTagNo.length - } - - const part = remainingTagNo.substring(0, nextDelimiterPos) - - if (!part) { - return `Empty part for field '${field.label}'.` - } - - if (field.expression) { - try { - let cleanPattern = field.expression.replace(/^\^/, '').replace(/\$$/, '') - const regex = new RegExp(`^${cleanPattern}$`) - - if (!regex.test(part)) { - return `Part '${part}' for field '${field.label}' does not match the pattern '${field.expression}'.` - } - } catch (error) { - console.error(`Invalid regex pattern: ${field.expression}`, error) - return `Invalid pattern for field '${field.label}': ${field.expression}` - } - } - - if (field.type === "select" && field.options && field.options.length > 0) { - const validValues = field.options.map(opt => opt.value) - if (!validValues.includes(part)) { - return `'${part}' is not a valid value for field '${field.label}'. Valid options: ${validValues.join(", ")}.` - } - } - - if (delimiter && nextDelimiterPos < remainingTagNo.length) { - remainingTagNo = remainingTagNo.substring(nextDelimiterPos + delimiter.length) - } else { - remainingTagNo = "" - break - } - } - - if (remainingTagNo) { - return `Tag number has extra parts: '${remainingTagNo}'.` - } - - return "" - } catch (error) { - console.error("Error validating tag number by class:", error) - return "Error validating tag number format." - } - }, [getTagTypeCodeByClassLabel, fetchSubfieldsByTagType]) - - // Import 파일 선택 - const handleImportClick = () => { - fileInputRef.current?.click() - } - - // Import 파일 처리 - const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { - const file = e.target.files?.[0] - if (!file) return - - e.target.value = "" - setIsPending(true) - - try { - const workbook = new ExcelJS.Workbook() - const arrayBuffer = await decryptWithServerAction(file) - await workbook.xlsx.load(arrayBuffer) - - const worksheet = workbook.worksheets[0] - const lastColIndex = worksheet.columnCount + 1 - worksheet.getRow(1).getCell(lastColIndex).value = "Error" - - const headerRowValues = worksheet.getRow(1).values as ExcelJS.CellValue[] - - // Excel header to accessor mapping - const excelHeaderToAccessor: Record<string, string> = {} - for (const col of columns) { - const meta = col.meta as { excelHeader?: string } | undefined - if (meta?.excelHeader) { - const accessor = col.id as string - excelHeaderToAccessor[meta.excelHeader] = accessor - } - } - - const accessorIndexMap: Record<string, number> = {} - for (let i = 1; i < headerRowValues.length; i++) { - const cellVal = String(headerRowValues[i] ?? "").trim() - if (!cellVal) continue - const accessor = excelHeaderToAccessor[cellVal] - if (accessor) { - accessorIndexMap[accessor] = i - } - } - - let errorCount = 0 - const importedRows: Tag[] = [] - const fileTagNos = new Set<string>() - const lastRow = worksheet.lastRow?.number || 1 - - for (let rowNum = 2; rowNum <= lastRow; rowNum++) { - const row = worksheet.getRow(rowNum) - const rowVals = row.values as ExcelJS.CellValue[] - if (!rowVals || rowVals.length <= 1) continue - - let errorMsg = "" - - const tagNoIndex = accessorIndexMap["tagNo"] - const classIndex = accessorIndexMap["class"] - - const tagNo = tagNoIndex ? String(rowVals[tagNoIndex] ?? "").trim() : "" - const classVal = classIndex ? String(rowVals[classIndex] ?? "").trim() : "" - - if (!tagNo) { - errorMsg += `Tag No is empty. ` - } - if (!classVal) { - errorMsg += `Class is empty. ` - } - - if (tagNo) { - const dup = tableData.find(t => t.tagNo === tagNo) - if (dup) { - errorMsg += `TagNo '${tagNo}' already exists. ` - } - - if (fileTagNos.has(tagNo)) { - errorMsg += `TagNo '${tagNo}' is duplicated within this file. ` - } else { - fileTagNos.add(tagNo) - } - } - - if (tagNo && classVal && !errorMsg) { - const classValidationError = await validateTagNumberByClass(tagNo, classVal) - if (classValidationError) { - errorMsg += classValidationError + " " - } - } - - if (errorMsg) { - row.getCell(lastColIndex).value = errorMsg.trim() - errorCount++ - } else { - const finalTagType = getTagTypeCodeByClassLabel(classVal) ?? "" - - importedRows.push({ - id: 0, - packageCode: packageCode, - projectCode: projectCode, - formId: null, - tagNo, - tagType: finalTagType, - class: classVal, - description: String(rowVals[accessorIndexMap["description"] ?? 0] ?? "").trim(), - createdAt: new Date(), - updatedAt: new Date(), - }) - } - } - - if (errorCount > 0) { - const outBuf = await workbook.xlsx.writeBuffer() - const errorFile = new Blob([outBuf]) - const url = URL.createObjectURL(errorFile) - const link = document.createElement("a") - link.href = url - link.download = "tag_import_errors.xlsx" - link.click() - URL.revokeObjectURL(url) - - toast.error(`There are ${errorCount} error row(s). Please see downloaded file.`) - return - } - - if (importedRows.length > 0) { - const result = await bulkCreateTags(importedRows, projectCode, packageCode) - if ("error" in result) { - toast.error(result.error) - } else { - toast.success(`${result.data.createdCount}개의 태그가 성공적으로 생성되었습니다.`) - router.refresh() - } - } - } catch (err) { - console.error(err) - toast.error("파일 업로드 중 오류가 발생했습니다.") - } finally { - setIsPending(false) - } - } - - // Export 함수 - const handleExport = async () => { - if (!tableRef.current) { - toast.error("테이블이 준비되지 않았습니다.") - return - } - - try { - setIsExporting(true) - await exportTagsToExcel(tableRef.current, packageCode, projectCode, { - filename: `Tags_${packageCode}_${projectCode}`, - excludeColumns: ["select", "actions", "createdAt", "updatedAt"], - }) - toast.success("태그 목록이 성공적으로 내보내졌습니다.") - } catch (error) { - console.error("Export error:", error) - toast.error("태그 목록 내보내기 중 오류가 발생했습니다.") - } finally { - setIsExporting(false) - } - } - - // Sync 함수 - const startGetTags = async () => { - try { - setIsSyncing(true) - - const response = await fetch('/api/cron/tags-plant/start', { - method: 'POST', - body: JSON.stringify({ - projectCode: projectCode, - packageCode: packageCode, - mode: selectedMode - }) - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to start tag import') - } - - const data = await response.json() - - if (data.syncId) { - setSyncId(data.syncId) - toast.info('Tag import started. This may take a while...') - startPolling(data.syncId) - } else { - throw new Error('No import ID returned from server') - } - } catch (error) { - console.error('Error starting tag import:', error) - toast.error( - error instanceof Error - ? error.message - : 'An error occurred while starting tag import' - ) - setIsSyncing(false) - } - } - - const startPolling = (id: string) => { - if (pollingRef.current) { - clearInterval(pollingRef.current) - } - - pollingRef.current = setInterval(async () => { - try { - const response = await fetch(`/api/cron/tags-plant/status?id=${id}`) - - if (!response.ok) { - throw new Error('Failed to get tag import status') - } - - const data = await response.json() - - if (data.status === 'completed') { - if (pollingRef.current) { - clearInterval(pollingRef.current) - pollingRef.current = null - } - - router.refresh() - setIsSyncing(false) - setSyncId(null) + // 3) useDataTable 훅으로 react-table 구성 + const { table } = useDataTable({ + data: data, // <-- 여기서 tableData 사용 + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + // sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + columnResizeMode: "onEnd", - toast.success( - `Tags imported successfully! ${data.result?.processedCount || 0} items processed.` - ) - } else if (data.status === 'failed') { - if (pollingRef.current) { - clearInterval(pollingRef.current) - pollingRef.current = null - } + }) - setIsSyncing(false) - setSyncId(null) - toast.error(data.error || 'Import failed') - } - } catch (error) { - console.error('Error checking importing status:', error) - } - }, 5000) - } + const [isCompact, setIsCompact] = React.useState<boolean>(false) - // rowAction 처리 - React.useEffect(() => { - if (rowAction?.type === "delete") { - handleDeleteRow(rowAction.row.original) - setRowAction(null) - } - }, [rowAction, handleDeleteRow]) - // Cleanup - React.useEffect(() => { - return () => { - if (pollingRef.current) { - clearInterval(pollingRef.current) - } - } + const handleCompactChange = React.useCallback((compact: boolean) => { + setIsCompact(compact) }, []) - - // 로딩 중 - if (isLoading) { - return ( - <div className="space-y-4"> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-[500px] w-full" /> - <Skeleton className="h-10 w-full" /> - </div> - ) - } - + + return ( <> - <ClientDataTable - data={tableData} - columns={columns} - advancedFilterFields={advancedFilterFields} - autoSizeColumns - onSelectedRowsChange={setSelectedRowsData} - clearSelection={clearSelection} - onTableReady={(table) => { - tableRef.current = table - }} - > - <div className="flex items-center gap-2"> - {/* 삭제 버튼 - 선택된 항목이 있을 때만 */} - {selectedRowCount > 0 && ( - <Button - variant="destructive" - size="sm" - onClick={handleBatchDelete} - > - <Trash2 className="mr-2 size-4" /> - Delete ({selectedRowCount}) - </Button> - )} - - {/* Get Tags 버튼 */} - <Button - variant="samsung" - size="sm" - onClick={startGetTags} - disabled={isSyncing} - > - <RefreshCcw className={`size-4 mr-2 ${isSyncing ? 'animate-spin' : ''}`} /> - <span className="hidden sm:inline"> - {isSyncing ? 'Syncing...' : 'Get Tags'} - </span> - </Button> - - {/* Add Tag 버튼 */} - <AddTagDialog - projectCode={projectCode} - packageCode={packageCode}/> + <DataTable + table={table} + compact={isCompact} - {/* Import 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleImportClick} - disabled={isPending || isExporting} - > - {isPending ? ( - <Loader2 className="size-4 mr-2 animate-spin" /> - ) : ( - <Upload className="size-4 mr-2" /> - )} - <span className="hidden sm:inline">Import</span> - </Button> - - {/* Export 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleExport} - disabled={isPending || isExporting || !tableRef.current} - > - {isExporting ? ( - <Loader2 className="size-4 mr-2 animate-spin" /> - ) : ( - <Download className="size-4 mr-2" /> - )} - <span className="hidden sm:inline">Export</span> - </Button> - </div> - </ClientDataTable> - - {/* Hidden file input */} - <input - ref={fileInputRef} - type="file" - accept=".xlsx,.xls" - className="hidden" - onChange={handleFileChange} - /> + floatingBar={<TagsTableFloatingBar table={table} selectedPackageId={selectedPackageId}/>} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + enableCompactToggle={true} + compactStorageKey="tagTableCompact" + onCompactChange={handleCompactChange} + > + {/* + 4) ToolbarActions에 tableData, setTableData 넘겨서 + import 시 상태 병합 + */} + <TagsTableToolbarActions + table={table} + selectedPackageId={selectedPackageId} + tableData={data} // <-- pass current data + selectedMode={selectedMode} + /> + </DataTableAdvancedToolbar> + </DataTable> - {/* Update Sheet */} <UpdateTagSheet open={rowAction?.type === "update"} - onOpenChange={(open) => { - if (!open) setRowAction(null) - }} + onOpenChange={() => setRowAction(null)} tag={rowAction?.row.original ?? null} - packageCode={packageCode} - projectCode={projectCode} - onUpdateSuccess={(updatedValues) => { - if (rowAction?.row.original?.tagNo) { - const tagNo = rowAction.row.original.tagNo - setTableData(prev => - prev.map(item => - item.tagNo === tagNo ? updatedValues : item - ) - ) - } - }} + selectedPackageId={selectedPackageId} /> - {/* Delete Dialog */} + <DeleteTagsDialog - tags={deleteTarget} - packageCode={packageCode} - projectCode={projectCode} - open={deleteDialogOpen} - onOpenChange={(open) => { - if (!open) { - setDeleteDialogOpen(false) - setDeleteTarget([]) - } - }} - onSuccess={handleDeleteSuccess} + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + tags={rowAction?.row.original ? [rowAction?.row.original] : []} showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + selectedPackageId={selectedPackageId} /> - - {/* Add Tag Dialog */} - {/* <AddTagDialog - projectCode={projectCode} - packageCode={packageCode} - open={addTagDialogOpen} - onOpenChange={setAddTagDialogOpen} - onSuccess={() => { - router.refresh() - }} - /> */} </> ) }
\ No newline at end of file diff --git a/lib/tags-plant/table/tags-export.tsx b/lib/tags-plant/table/tags-export.tsx index a3255a0b..fa85148d 100644 --- a/lib/tags-plant/table/tags-export.tsx +++ b/lib/tags-plant/table/tags-export.tsx @@ -15,8 +15,7 @@ import { getClassOptions } from "../service" */ export async function exportTagsToExcel( table: Table<Tag>, - packageCode: string, - projectCode: string, + selectedPackageId: number, { filename = "Tags", excludeColumns = ["select", "actions", "createdAt", "updatedAt"], @@ -43,7 +42,7 @@ export async function exportTagsToExcel( const worksheet = workbook.addWorksheet("Tags") // 3. Tag Class 옵션 가져오기 - const classOptions = await getClassOptions(packageCode, projectCode) + const classOptions = await getClassOptions(selectedPackageId) // 4. 유효성 검사 시트 생성 const validationSheet = workbook.addWorksheet("ValidationData") diff --git a/lib/tags-plant/table/tags-table-floating-bar.tsx b/lib/tags-plant/table/tags-table-floating-bar.tsx index eadbfb12..8d55b7ac 100644 --- a/lib/tags-plant/table/tags-table-floating-bar.tsx +++ b/lib/tags-plant/table/tags-table-floating-bar.tsx @@ -36,13 +36,12 @@ import { Tag } from "@/db/schema/vendorData" interface TagsTableFloatingBarProps { table: Table<Tag> - packageCode: string - projectCode: string + selectedPackageId: number } -export function TagsTableFloatingBar({ table, packageCode, projectCode}: TagsTableFloatingBarProps) { +export function TagsTableFloatingBar({ table, selectedPackageId }: TagsTableFloatingBarProps) { const rows = table.getFilteredSelectedRowModel().rows const [isPending, startTransition] = React.useTransition() diff --git a/lib/tags-plant/table/tags-table-toolbar-actions.tsx b/lib/tags-plant/table/tags-table-toolbar-actions.tsx index c80a600e..cc2d82b4 100644 --- a/lib/tags-plant/table/tags-table-toolbar-actions.tsx +++ b/lib/tags-plant/table/tags-table-toolbar-actions.tsx @@ -52,8 +52,7 @@ interface TagsTableToolbarActionsProps { /** react-table 객체 */ table: Table<Tag> /** 현재 선택된 패키지 ID */ - packageCode: string - projectCode: string + selectedPackageId: number /** 현재 태그 목록(상태) */ tableData: Tag[] /** 태그 목록을 갱신하는 setState */ @@ -69,8 +68,7 @@ interface TagsTableToolbarActionsProps { */ export function TagsTableToolbarActions({ table, - packageCode, - projectCode, + selectedPackageId, tableData, selectedMode }: TagsTableToolbarActionsProps) { @@ -96,7 +94,7 @@ export function TagsTableToolbarActions({ React.useEffect(() => { const loadClassOptions = async () => { try { - const options = await getClassOptions(packageCode, projectCode) + const options = await getClassOptions(selectedPackageId) setClassOptions(options) } catch (error) { console.error("Failed to load class options:", error) @@ -104,7 +102,7 @@ export function TagsTableToolbarActions({ } loadClassOptions() - }, [packageCode, projectCode]) + }, [selectedPackageId]) // 숨겨진 <input>을 클릭 function handleImportClick() { @@ -137,11 +135,12 @@ export function TagsTableToolbarActions({ const [projectId, setProjectId] = React.useState<number | null>(null); + // Add useEffect to fetch projectId when selectedPackageId changes React.useEffect(() => { const fetchProjectId = async () => { - if (packageCode && projectCode) { + if (selectedPackageId) { try { - const pid = await getProjectIdFromContractItemId(projectCode ); + const pid = await getProjectIdFromContractItemId(selectedPackageId); setProjectId(pid); } catch (error) { console.error("Failed to fetch project ID:", error); @@ -151,7 +150,7 @@ export function TagsTableToolbarActions({ }; fetchProjectId(); - }, [projectCode]); + }, [selectedPackageId]); // 특정 attributesId에 대한 옵션 가져오기 const fetchOptions = React.useCallback(async (attributesId: string): Promise<TagOption[]> => { @@ -196,7 +195,7 @@ export function TagsTableToolbarActions({ } try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode) + const { subFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) // API 응답을 SubFieldDef 형식으로 변환 const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ @@ -479,7 +478,7 @@ export function TagsTableToolbarActions({ if (tagNo) { // 이미 tableData 내 존재 여부 const dup = tableData.find( - (t) => t.tagNo === tagNo + (t) => t.contractItemId === selectedPackageId && t.tagNo === tagNo ) if (dup) { errorMsg += `TagNo '${tagNo}' already exists. ` @@ -524,8 +523,7 @@ export function TagsTableToolbarActions({ // 정상 행을 importedRows에 추가 importedRows.push({ id: 0, // 임시 - packageCode: packageCode, - projectCode: projectCode, + contractItemId: selectedPackageId, formId: null, tagNo, tagType: finalTagType, // ← 코드로 저장할지, Description으로 저장할지 결정 @@ -554,7 +552,7 @@ export function TagsTableToolbarActions({ // 정상 행이 있으면 태그 생성 요청 if (importedRows.length > 0) { - const result = await bulkCreateTags(importedRows, projectCode, packageCode); + const result = await bulkCreateTags(importedRows, selectedPackageId); if ("error" in result) { toast.error(result.error); } else { @@ -577,8 +575,8 @@ export function TagsTableToolbarActions({ setIsExporting(true) // 유효성 검사가 포함된 새로운 엑셀 내보내기 함수 호출 - await exportTagsToExcel(table, packageCode,projectCode, { - filename: `Tags_${packageCode}_${projectCode}`, + await exportTagsToExcel(table, selectedPackageId, { + filename: `Tags_${selectedPackageId}`, excludeColumns: ["select", "actions", "createdAt", "updatedAt"], }) @@ -596,11 +594,10 @@ export function TagsTableToolbarActions({ setIsLoading(true) // API 엔드포인트 호출 - 작업 시작만 요청 - const response = await fetch('/api/cron/tags-plant/start', { + const response = await fetch('/api/cron/tags/start', { method: 'POST', body: JSON.stringify({ - projectCode: projectCode, - packageCode: packageCode, + packageId: selectedPackageId, mode: selectedMode // 모드 정보 추가 }) }) @@ -641,7 +638,7 @@ export function TagsTableToolbarActions({ // 5초마다 상태 확인 pollingRef.current = setInterval(async () => { try { - const response = await fetch(`/api/cron/tags-plant/status?id=${id}`) + const response = await fetch(`/api/cron/tags/status?id=${id}`) if (!response.ok) { throw new Error('Failed to get tag import status') @@ -702,8 +699,7 @@ export function TagsTableToolbarActions({ .getFilteredSelectedRowModel() .rows.map((row) => row.original)} onSuccess={() => table.toggleAllRowsSelected(false)} - projectCode={projectCode} - packageCode={packageCode} + selectedPackageId={selectedPackageId} /> ) : null} <Button @@ -719,7 +715,7 @@ export function TagsTableToolbarActions({ </span> </Button> - <AddTagDialog projectCode={projectCode} packageCode={packageCode} /> + <AddTagDialog selectedPackageId={selectedPackageId} /> {/* Import */} <Button diff --git a/lib/tags-plant/table/update-tag-sheet.tsx b/lib/tags-plant/table/update-tag-sheet.tsx index 2be1e732..613abaa9 100644 --- a/lib/tags-plant/table/update-tag-sheet.tsx +++ b/lib/tags-plant/table/update-tag-sheet.tsx @@ -50,7 +50,7 @@ import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { Tag } from "@/db/schema/vendorData" -import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags-plant/service" +import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags/service" // SubFieldDef 인터페이스 interface SubFieldDef { @@ -84,11 +84,10 @@ type UpdateTagSchema = z.infer<typeof updateTagSchema> & Record<string, string> interface UpdateTagSheetProps extends React.ComponentPropsWithRef<typeof Sheet> { tag: Tag | null - packageCode: string - projectCode: string + selectedPackageId: number } -export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: UpdateTagSheetProps) { +export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([]) const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null) @@ -111,7 +110,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat setIsLoadingClasses(true) try { - const result = await getClassOptions(packageCode, projectCode) + const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { toast.error("클래스 옵션을 불러오는데 실패했습니다.") @@ -165,7 +164,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { setIsLoadingSubFields(true) try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -222,7 +221,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat ), } - const result = await updateTag(tagData, projectCode,packageCode ) + const result = await updateTag(tagData, selectedPackageId) if ("error" in result) { toast.error(result.error) diff --git a/lib/vendor-data/services.ts b/lib/vendor-data/services.ts index fe4e56ae..8c8b21d2 100644 --- a/lib/vendor-data/services.ts +++ b/lib/vendor-data/services.ts @@ -62,8 +62,6 @@ export async function getVendorProjectsAndContracts( itemId: contractItems.id, itemName: items.itemName, - packageCode: items.packageCode, - packageName: items.description, }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) @@ -128,94 +126,3 @@ export async function getVendorProjectsAndContracts( return Array.from(projectMap.values()) } -interface ProjectWithPackages { - projectId: number - projectCode: string - projectName: string - projectType: "ship" | "plant" - packages: { - packageCode: string - packageName: string | null - }[] -} - -export async function getVendorProjectsWithPackages( - vendorId?: number, - projectType?: "ship" | "plant" -): Promise<ProjectWithPackages[]> { - // 세션에서 도메인 정보 가져오기 - const session = await getServerSession(authOptions) - - // EVCP 도메인일 때만 전체 조회 - const isEvcpDomain = session?.user?.domain === "evcp" - - // where 조건들을 배열로 관리 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const whereConditions: any[] = [] - - // vendorId 조건 추가 - if (!isEvcpDomain && vendorId) { - whereConditions.push(eq(contracts.vendorId, vendorId)) - } - - // projectType 조건 추가 - if (projectType) { - whereConditions.push(eq(projects.type, projectType)) - } - - const query = db - .select({ - projectId: projects.id, - projectCode: projects.code, - projectName: projects.name, - projectType: projects.type, - - packageCode: items.packageCode, - packageName: items.description, - }) - .from(contracts) - .innerJoin(projects, eq(contracts.projectId, projects.id)) - .innerJoin(contractItems, eq(contractItems.contractId, contracts.id)) - .innerJoin(items, eq(contractItems.itemId, items.id)) - - // 조건이 있으면 where 절 추가 - if (whereConditions.length > 0) { - query.where(and(...whereConditions)) - } - - const rows = await query - - const projectMap = new Map<number, ProjectWithPackages>() - - for (const row of rows) { - // 1) 프로젝트 그룹 찾기 - let projectEntry = projectMap.get(row.projectId) - if (!projectEntry) { - // 새 프로젝트 항목 생성 - projectEntry = { - projectId: row.projectId, - projectCode: row.projectCode, - projectName: row.projectName, - projectType: row.projectType, - packages: [], - } - projectMap.set(row.projectId, projectEntry) - } - - // 2) 프로젝트의 packages 배열에 패키지 추가 (중복 체크) - // packageCode가 같은 항목이 이미 존재하는지 확인 - const existingPackage = projectEntry.packages.find( - (pkg) => pkg.packageCode === row.packageCode - ) - - // 같은 packageCode가 없는 경우에만 추가 - if (!existingPackage) { - projectEntry.packages.push({ - packageCode: row.packageCode, - packageName: row.packageName, - }) - } - } - - return Array.from(projectMap.values()) -}
\ No newline at end of file diff --git a/lib/vendor-document/service.ts b/lib/vendor-document/service.ts index 48e3fa3f..bf2b0b7a 100644 --- a/lib/vendor-document/service.ts +++ b/lib/vendor-document/service.ts @@ -2,14 +2,14 @@ import { eq, SQL } from "drizzle-orm" import db from "@/db/db" -import { stageSubmissions, stageDocuments, stageIssueStages,documentAttachments, documents, issueStages, revisions, stageDocumentsView,vendorDocumentsView ,stageSubmissionAttachments, StageIssueStage, StageDocumentsView, StageDocument,} from "@/db/schema/vendorDocu" +import { documentAttachments, documents, issueStages, revisions, vendorDocumentsView } from "@/db/schema/vendorDocu" import { GetVendorDcoumentsSchema } from "./validations" import { unstable_cache } from "@/lib/unstable-cache"; import { filterColumns } from "@/lib/filter-columns"; import { getErrorMessage } from "@/lib/handle-error"; import { asc, desc, ilike, inArray, and, gte, lte, not, or , isNotNull, isNull} from "drizzle-orm"; import { countVendorDocuments, selectVendorDocuments } from "./repository" -import { contractItems, projects, items,contracts } from "@/db/schema" +import { contractItems } from "@/db/schema" import { saveFile } from "../file-stroage" import path from "path" @@ -494,706 +494,4 @@ export async function fetchRevisionsByStageParams( console.error("Error fetching revisions:", error); return []; } -} - -// 타입 정의 -type SubmissionInfo = { - id: number; - revisionNumber: number; - revisionCode: string; - revisionType: string; - submissionStatus: string; - submittedBy: string; - submittedAt: Date; - reviewStatus: string | null; - buyerSystemStatus: string | null; - syncStatus: string; -}; - -type AttachmentInfo = { - id: number; - fileName: string; - originalFileName: string; - fileSize: number; - fileType: string | null; - storageUrl: string | null; - syncStatus: string; - buyerSystemStatus: string | null; - uploadedAt: Date; -}; - -// Server Action: Fetch documents by projectCode and packageCode -export async function fetchDocumentsByProjectAndPackage( - projectCode: string, - packageCode: string -): Promise<StageDocument[]> { - try { - // First, find the project by code - const projectResult = await db - .select({ id: projects.id }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectResult.length) { - return []; - } - - const projectId = projectResult[0].id; - - // Find contract through contractItems joined with items table - const contractItemResult = await db - .select({ - contractId: contractItems.contractId - }) - .from(contractItems) - .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) - .innerJoin(items, eq(contractItems.itemId, items.id)) - .where( - and( - eq(contracts.projectId, projectId), - eq(items.packageCode, packageCode) - ) - ) - .limit(1); - - if (!contractItemResult.length) { - return []; - } - - const contractId = contractItemResult[0].contractId; - - // Get stage documents - const docsResult = await db - .select({ - id: stageDocuments.id, - docNumber: stageDocuments.docNumber, - title: stageDocuments.title, - vendorDocNumber: stageDocuments.vendorDocNumber, - status: stageDocuments.status, - issuedDate: stageDocuments.issuedDate, - docClass: stageDocuments.docClass, - projectId: stageDocuments.projectId, - vendorId: stageDocuments.vendorId, - contractId: stageDocuments.contractId, - buyerSystemStatus: stageDocuments.buyerSystemStatus, - buyerSystemComment: stageDocuments.buyerSystemComment, - lastSyncedAt: stageDocuments.lastSyncedAt, - syncStatus: stageDocuments.syncStatus, - syncError: stageDocuments.syncError, - syncVersion: stageDocuments.syncVersion, - lastModifiedBy: stageDocuments.lastModifiedBy, - createdAt: stageDocuments.createdAt, - updatedAt: stageDocuments.updatedAt, - }) - .from(stageDocuments) - .where( - and( - eq(stageDocuments.projectId, projectId), - eq(stageDocuments.contractId, contractId), - eq(stageDocuments.status, "ACTIVE") - ) - ) - .orderBy(stageDocuments.docNumber); - - return docsResult; - } catch (error) { - console.error("Error fetching documents:", error); - return []; - } -} - -// Server Action: Fetch stages by documentId -export async function fetchStagesByDocumentIdPlant( - documentId: number -): Promise<StageIssueStage[]> { - try { - const stagesResult = await db - .select({ - id: stageIssueStages.id, - documentId: stageIssueStages.documentId, - stageName: stageIssueStages.stageName, - planDate: stageIssueStages.planDate, - actualDate: stageIssueStages.actualDate, - stageStatus: stageIssueStages.stageStatus, - stageOrder: stageIssueStages.stageOrder, - priority: stageIssueStages.priority, - assigneeId: stageIssueStages.assigneeId, - assigneeName: stageIssueStages.assigneeName, - reminderDays: stageIssueStages.reminderDays, - description: stageIssueStages.description, - notes: stageIssueStages.notes, - createdAt: stageIssueStages.createdAt, - updatedAt: stageIssueStages.updatedAt, - }) - .from(stageIssueStages) - .where(eq(stageIssueStages.documentId, documentId)) - .orderBy(stageIssueStages.stageOrder, stageIssueStages.stageName); - - return stagesResult; - } catch (error) { - console.error("Error fetching stages:", error); - return []; - } -} - -// Server Action: Fetch submissions (revisions) by documentId and stageName -export async function fetchSubmissionsByStageParams( - documentId: number, - stageName: string -): Promise<SubmissionInfo[]> { - try { - // First, find the stageId - const stageResult = await db - .select({ id: stageIssueStages.id }) - .from(stageIssueStages) - .where( - and( - eq(stageIssueStages.documentId, documentId), - eq(stageIssueStages.stageName, stageName) - ) - ) - .limit(1); - - if (!stageResult.length) { - return []; - } - - const stageId = stageResult[0].id; - - // Then, get submissions for this stage - const submissionsResult = await db - .select({ - id: stageSubmissions.id, - revisionNumber: stageSubmissions.revisionNumber, - revisionCode: stageSubmissions.revisionCode, - revisionType: stageSubmissions.revisionType, - submissionStatus: stageSubmissions.submissionStatus, - submittedBy: stageSubmissions.submittedBy, - submittedAt: stageSubmissions.submittedAt, - reviewStatus: stageSubmissions.reviewStatus, - buyerSystemStatus: stageSubmissions.buyerSystemStatus, - syncStatus: stageSubmissions.syncStatus, - }) - .from(stageSubmissions) - .where(eq(stageSubmissions.stageId, stageId)) - .orderBy(stageSubmissions.revisionNumber); - - return submissionsResult; - } catch (error) { - console.error("Error fetching submissions:", error); - return []; - } -} - -// View를 활용한 더 효율적인 조회 -export async function fetchDocumentsViewByProjectAndPackage( - projectCode: string, - packageCode: string -): Promise<StageDocumentsView[]> { - try { - // First, find the project by code - const projectResult = await db - .select({ id: projects.id }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectResult.length) { - return []; - } - - const projectId = projectResult[0].id; - - // Find contract through contractItems joined with items - const contractItemResult = await db - .select({ - contractId: contractItems.contractId - }) - .from(contractItems) - .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) - .innerJoin(items, eq(contractItems.itemId, items.id)) - .where( - and( - eq(contracts.projectId, projectId), - eq(items.packageCode, packageCode) - ) - ) - .limit(1); - - if (!contractItemResult.length) { - return []; - } - - const contractId = contractItemResult[0].contractId; - - // Use the view for enriched data (includes progress, current stage, etc.) - const documentsViewResult = await db - .select() - .from(stageDocumentsView) - .where( - and( - eq(stageDocumentsView.projectId, projectId), - eq(stageDocumentsView.contractId, contractId), - eq(stageDocumentsView.status, "ACTIVE") - ) - ) - .orderBy(stageDocumentsView.docNumber); - - return documentsViewResult; - } catch (error) { - console.error("Error fetching documents view:", error); - return []; - } -} - -// Server Action: Fetch submission attachments by submissionId -export async function fetchAttachmentsBySubmissionId( - submissionId: number -): Promise<AttachmentInfo[]> { - try { - const attachmentsResult = await db - .select({ - id: stageSubmissionAttachments.id, - fileName: stageSubmissionAttachments.fileName, - originalFileName: stageSubmissionAttachments.originalFileName, - fileSize: stageSubmissionAttachments.fileSize, - fileType: stageSubmissionAttachments.fileType, - storageUrl: stageSubmissionAttachments.storageUrl, - syncStatus: stageSubmissionAttachments.syncStatus, - buyerSystemStatus: stageSubmissionAttachments.buyerSystemStatus, - uploadedAt: stageSubmissionAttachments.uploadedAt, - }) - .from(stageSubmissionAttachments) - .where( - and( - eq(stageSubmissionAttachments.submissionId, submissionId), - eq(stageSubmissionAttachments.status, "ACTIVE") - ) - ) - .orderBy(stageSubmissionAttachments.uploadedAt); - - return attachmentsResult; - } catch (error) { - console.error("Error fetching attachments:", error); - return []; - } -} - -// 추가 헬퍼: 특정 제출의 상세 정보 (첨부파일 포함) -export async function getSubmissionWithAttachments(submissionId: number) { - try { - const [submission] = await db - .select({ - id: stageSubmissions.id, - stageId: stageSubmissions.stageId, - documentId: stageSubmissions.documentId, - revisionNumber: stageSubmissions.revisionNumber, - revisionCode: stageSubmissions.revisionCode, - revisionType: stageSubmissions.revisionType, - submissionStatus: stageSubmissions.submissionStatus, - submittedBy: stageSubmissions.submittedBy, - submittedByEmail: stageSubmissions.submittedByEmail, - submittedAt: stageSubmissions.submittedAt, - reviewedBy: stageSubmissions.reviewedBy, - reviewedAt: stageSubmissions.reviewedAt, - submissionTitle: stageSubmissions.submissionTitle, - submissionDescription: stageSubmissions.submissionDescription, - reviewStatus: stageSubmissions.reviewStatus, - reviewComments: stageSubmissions.reviewComments, - vendorId: stageSubmissions.vendorId, - totalFiles: stageSubmissions.totalFiles, - buyerSystemStatus: stageSubmissions.buyerSystemStatus, - syncStatus: stageSubmissions.syncStatus, - createdAt: stageSubmissions.createdAt, - updatedAt: stageSubmissions.updatedAt, - }) - .from(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)) - .limit(1); - - if (!submission) { - return null; - } - - const attachments = await fetchAttachmentsBySubmissionId(submissionId); - - return { - ...submission, - attachments, - }; - } catch (error) { - console.error("Error getting submission with attachments:", error); - return null; - } -} - - -interface CreateSubmissionResult { - success: boolean; - error?: string; - submissionId?: number; -} - -export async function createSubmissionAction( - formData: FormData -): Promise<CreateSubmissionResult> { - try { - // Extract form data - const documentId = formData.get("documentId") as string; - const stageName = formData.get("stageName") as string; - const revisionCode = formData.get("revisionCode") as string; - const customFileName = formData.get("customFileName") as string; - const submittedBy = formData.get("submittedBy") as string; - const submittedByEmail = formData.get("submittedByEmail") as string | null; - const submissionTitle = formData.get("submissionTitle") as string | null; - const submissionDescription = formData.get("submissionDescription") as string | null; - const vendorId = formData.get("vendorId") as string; - const attachment = formData.get("attachment") as File | null; - - // Validate required fields - if (!documentId || !stageName || !revisionCode || !submittedBy || !vendorId) { - return { - success: false, - error: "Missing required fields", - }; - } - - const parsedDocumentId = parseInt(documentId, 10); - const parsedVendorId = parseInt(vendorId, 10); - - // Validate parsed numbers - if (isNaN(parsedDocumentId) || isNaN(parsedVendorId)) { - return { - success: false, - error: "Invalid documentId or vendorId", - }; - } - - // Find the document - const [document] = await db - .select() - .from(stageDocuments) - .where(eq(stageDocuments.id, parsedDocumentId)) - .limit(1); - - if (!document) { - return { - success: false, - error: "Document not found", - }; - } - - // Find the stage - const [stage] = await db - .select() - .from(stageIssueStages) - .where( - and( - eq(stageIssueStages.documentId, parsedDocumentId), - eq(stageIssueStages.stageName, stageName) - ) - ) - .limit(1); - - if (!stage) { - return { - success: false, - error: `Stage "${stageName}" not found for this document`, - }; - } - - const stageId = stage.id; - - // Get the latest revision number for this stage - const existingSubmissions = await db - .select({ - revisionNumber: stageSubmissions.revisionNumber, - }) - .from(stageSubmissions) - .where(eq(stageSubmissions.stageId, stageId)) - .orderBy(desc(stageSubmissions.revisionNumber)) - .limit(1); - - const nextRevisionNumber = existingSubmissions.length > 0 - ? existingSubmissions[0].revisionNumber + 1 - : 1; - - // Check if revision code already exists for this stage - const [existingRevisionCode] = await db - .select() - .from(stageSubmissions) - .where( - and( - eq(stageSubmissions.stageId, stageId), - eq(stageSubmissions.revisionCode, revisionCode) - ) - ) - .limit(1); - - if (existingRevisionCode) { - return { - success: false, - error: `Revision code "${revisionCode}" already exists for this stage`, - }; - } - - // Get vendor code from vendors table - const [vendor] = await db - .select({ vendorCode: vendors.vendorCode }) - .from(vendors) - .where(eq(vendors.id, parsedVendorId)) - .limit(1); - - const vendorCode = vendor?.vendorCode || parsedVendorId.toString(); - - // Determine revision type - const revisionType = nextRevisionNumber === 1 ? "INITIAL" : "RESUBMISSION"; - - // Create the submission - const [newSubmission] = await db - .insert(stageSubmissions) - .values({ - stageId, - documentId: parsedDocumentId, - revisionNumber: nextRevisionNumber, - revisionCode, - revisionType, - submissionStatus: "SUBMITTED", - submittedBy, - submittedByEmail: submittedByEmail || undefined, - submittedAt: new Date(), - submissionTitle: submissionTitle || undefined, - submissionDescription: submissionDescription || undefined, - vendorId: parsedVendorId, - vendorCode, - totalFiles: attachment ? 1 : 0, - totalFileSize: attachment ? attachment.size : 0, - syncStatus: "pending", - syncVersion: 0, - lastModifiedBy: "EVCP", - totalFilesToSync: attachment ? 1 : 0, - syncedFilesCount: 0, - failedFilesCount: 0, - }) - .returning(); - - if (!newSubmission) { - return { - success: false, - error: "Failed to create submission", - }; - } - - // Upload attachment if provided - if (attachment) { - try { - // Generate unique filename - const fileExtension = customFileName.split(".").pop() || "docx"; - const timestamp = Date.now(); - const randomString = crypto.randomBytes(8).toString("hex"); - const uniqueFileName = `submissions/${parsedDocumentId}/${stageId}/${timestamp}_${randomString}.${fileExtension}`; - - // Calculate checksum - const buffer = await attachment.arrayBuffer(); - const checksum = crypto - .createHash("md5") - .update(Buffer.from(buffer)) - .digest("hex"); - - // Upload to Vercel Blob (or your storage solution) - const blob = await put(uniqueFileName, attachment, { - access: "public", - contentType: attachment.type || "application/octet-stream", - }); - - // Create attachment record - await db.insert(stageSubmissionAttachments).values({ - submissionId: newSubmission.id, - fileName: uniqueFileName, - originalFileName: customFileName, - fileType: attachment.type || "application/octet-stream", - fileExtension, - fileSize: attachment.size, - storageType: "S3", - storagePath: blob.url, - storageUrl: blob.url, - mimeType: attachment.type || "application/octet-stream", - checksum, - documentType: "DOCUMENT", - uploadedBy: submittedBy, - uploadedAt: new Date(), - status: "ACTIVE", - syncStatus: "pending", - syncVersion: 0, - lastModifiedBy: "EVCP", - isPublic: false, - }); - - // Update submission with file info - await db - .update(stageSubmissions) - .set({ - totalFiles: 1, - totalFileSize: attachment.size, - totalFilesToSync: 1, - updatedAt: new Date(), - }) - .where(eq(stageSubmissions.id, newSubmission.id)); - } catch (uploadError) { - console.error("Error uploading attachment:", uploadError); - - // Rollback: Delete the submission if file upload fails - await db - .delete(stageSubmissions) - .where(eq(stageSubmissions.id, newSubmission.id)); - - return { - success: false, - error: uploadError instanceof Error - ? `File upload failed: ${uploadError.message}` - : "File upload failed", - }; - } - } - - // Update stage status to SUBMITTED - await db - .update(stageIssueStages) - .set({ - stageStatus: "SUBMITTED", - updatedAt: new Date(), - }) - .where(eq(stageIssueStages.id, stageId)); - - // Update document's last modified info - await db - .update(stageDocuments) - .set({ - lastModifiedBy: "EVCP", - syncVersion: document.syncVersion + 1, - updatedAt: new Date(), - }) - .where(eq(stageDocuments.id, parsedDocumentId)); - - // Revalidate relevant paths - revalidatePath(`/projects/${document.projectId}/documents`); - revalidatePath(`/vendor/documents`); - revalidatePath(`/vendor/submissions`); - - return { - success: true, - submissionId: newSubmission.id, - }; - } catch (error) { - console.error("Error creating submission:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error occurred", - }; - } -} - -// Additional helper: Update submission status -export async function updateSubmissionStatus( - submissionId: number, - status: string, - reviewedBy?: string, - reviewComments?: string -): Promise<CreateSubmissionResult> { - try { - const reviewStatus = - status === "APPROVED" ? "APPROVED" : - status === "REJECTED" ? "REJECTED" : - "PENDING"; - - await db - .update(stageSubmissions) - .set({ - submissionStatus: status, - reviewStatus, - reviewComments: reviewComments || undefined, - reviewedBy: reviewedBy || undefined, - reviewedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(stageSubmissions.id, submissionId)); - - // If approved, update stage status - if (status === "APPROVED") { - const [submission] = await db - .select({ stageId: stageSubmissions.stageId }) - .from(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)) - .limit(1); - - if (submission) { - await db - .update(stageIssueStages) - .set({ - stageStatus: "APPROVED", - actualDate: new Date().toISOString().split('T')[0], - updatedAt: new Date(), - }) - .where(eq(stageIssueStages.id, submission.stageId)); - } - } - - return { success: true }; - } catch (error) { - console.error("Error updating submission status:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Failed to update submission status" - }; - } -} - -// Helper: Delete submission -export async function deleteSubmissionAction( - submissionId: number -): Promise<CreateSubmissionResult> { - try { - // Get submission info first - const [submission] = await db - .select() - .from(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)) - .limit(1); - - if (!submission) { - return { - success: false, - error: "Submission not found", - }; - } - - // Delete attachments from storage - const attachments = await db - .select() - .from(stageSubmissionAttachments) - .where(eq(stageSubmissionAttachments.submissionId, submissionId)); - - // TODO: Delete files from blob storage - // for (const attachment of attachments) { - // await del(attachment.storageUrl); - // } - - // Delete submission (cascade will delete attachments) - await db - .delete(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)); - - // Revalidate paths - revalidatePath(`/vendor/documents`); - revalidatePath(`/vendor/submissions`); - - return { success: true }; - } catch (error) { - console.error("Error deleting submission:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Failed to delete submission", - }; - } }
\ No newline at end of file diff --git a/types/table.d.ts b/types/table.d.ts index 9fc96687..d4053cf1 100644 --- a/types/table.d.ts +++ b/types/table.d.ts @@ -54,7 +54,7 @@ export type Filter<TData> = Prettify< export interface DataTableRowAction<TData> { row: Row<TData> - type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission" | "resend" + type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission" } export interface QueryBuilderOpts { |
