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 /components | |
| parent | 95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff) | |
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
Diffstat (limited to 'components')
15 files changed, 1108 insertions, 800 deletions
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> |
