summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/bidding/ProjectSelectorBid.tsx6
-rw-r--r--components/bidding/manage/bidding-items-editor.tsx3
-rw-r--r--components/client-data-table/data-table.tsx300
-rw-r--r--components/common/permission-checker.tsx36
-rw-r--r--components/form-data-plant/delete-form-data-dialog.tsx9
-rw-r--r--components/form-data-plant/form-data-report-batch-dialog.tsx21
-rw-r--r--components/form-data-plant/form-data-report-dialog.tsx21
-rw-r--r--components/form-data-plant/form-data-report-temp-upload-dialog.tsx12
-rw-r--r--components/form-data-plant/form-data-report-temp-upload-tab.tsx7
-rw-r--r--components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx19
-rw-r--r--components/form-data-plant/form-data-table.tsx91
-rw-r--r--components/form-data-plant/import-excel-form.tsx13
-rw-r--r--components/form-data-plant/publish-dialog.tsx226
-rw-r--r--components/form-data-plant/spreadJS-dialog.tsx15
-rw-r--r--components/form-data-plant/update-form-sheet.tsx9
-rw-r--r--components/vendor-data-plant/project-swicher.tsx163
-rw-r--r--components/vendor-data-plant/sidebar.tsx479
-rw-r--r--components/vendor-data-plant/vendor-data-container.tsx523
18 files changed, 843 insertions, 1110 deletions
diff --git a/components/bidding/ProjectSelectorBid.tsx b/components/bidding/ProjectSelectorBid.tsx
index 0fc567b3..a87c8dce 100644
--- a/components/bidding/ProjectSelectorBid.tsx
+++ b/components/bidding/ProjectSelectorBid.tsx
@@ -13,13 +13,15 @@ interface ProjectSelectorProps {
onProjectSelect: (project: Project | null) => void;
placeholder?: string;
filterType?: string; // 옵션으로 필터 타입 지정 가능
+ disabled?: boolean;
}
export function ProjectSelector({
selectedProjectId,
onProjectSelect,
placeholder = "프로젝트 선택...",
- filterType
+ filterType,
+ disabled = false
}: ProjectSelectorProps) {
const [open, setOpen] = React.useState(false)
const [searchTerm, setSearchTerm] = React.useState("")
@@ -95,7 +97,7 @@ export function ProjectSelector({
role="combobox"
aria-expanded={open}
className="w-full justify-between"
- disabled={isLoading}
+ disabled={isLoading || disabled}
>
{isLoading ? (
"프로젝트 로딩 중..."
diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx
index 9d858f40..90e512d2 100644
--- a/components/bidding/manage/bidding-items-editor.tsx
+++ b/components/bidding/manage/bidding-items-editor.tsx
@@ -868,6 +868,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
}}
title="1회성 품목 선택"
description="1회성 품목을 검색하고 선택해주세요."
+ disabled={readonly}
/>
) : (
<MaterialGroupSelectorDialogSingle
@@ -893,6 +894,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
}}
title="자재그룹 선택"
description="자재그룹을 검색하고 선택해주세요."
+ disabled={readonly}
/>
)}
</td>
@@ -928,6 +930,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
}}
title="자재 선택"
description="자재를 검색하고 선택해주세요."
+ disabled={readonly}
/>
</td>
<td className="border-r px-3 py-2">
diff --git a/components/client-data-table/data-table.tsx b/components/client-data-table/data-table.tsx
index 3e009302..371a1dab 100644
--- a/components/client-data-table/data-table.tsx
+++ b/components/client-data-table/data-table.tsx
@@ -49,8 +49,9 @@ interface DataTableProps<TData, TValue> {
children?: React.ReactNode
/** 선택 상태 초기화 트리거 */
clearSelection?: boolean
- initialColumnPinning?: ColumnPinningState // 추가
-
+ initialColumnPinning?: ColumnPinningState
+ /** Table 인스턴스를 상위 컴포넌트에 전달하는 콜백 */
+ onTableReady?: (table: Table<TData>) => void
}
export function ClientDataTable<TData, TValue>({
@@ -63,7 +64,8 @@ export function ClientDataTable<TData, TValue>({
maxHeight,
onSelectedRowsChange,
clearSelection,
- initialColumnPinning
+ initialColumnPinning,
+ onTableReady
}: DataTableProps<TData, TValue>) {
// (1) React Table 상태
@@ -118,6 +120,13 @@ 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
@@ -164,6 +173,7 @@ export function ClientDataTable<TData, TValue>({
}),
}
}
+
// 🎯 테이블 총 너비 계산
const getTableWidth = React.useCallback(() => {
const totalSize = table.getCenterTotalSize() + table.getLeftTotalSize() + table.getRightTotalSize()
@@ -206,174 +216,172 @@ 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()
- )}
-
- {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */}
- {header.column.getCanResize() && !('columns' in header.column.columnDef) && (
- <DataTableResizer header={header} />
+ 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()
)}
- </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 객체
+
+ {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */}
+ {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 객체
- // 컬럼 라벨 가져오기
- 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.row}
- data-state={row.getIsSelected() && "selected"}
+ className={compactStyles.groupRow}
+ data-state={row.getIsExpanded() && "expanded"}
>
- {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}
+ {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */}
+ <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={{
- ...getPinnedStyle(cell.column, false), // 🎯 바디 셀임을 명시
- width: cell.column.getSize() // 🎯 width 별도 설정
+ // row.depth: 0이면 top-level, 1이면 그 하위 등
+ marginLeft: `${row.depth * 1.5}rem`,
}}
>
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
+ {row.getIsExpanded() ? (
+ <ChevronUp size={compact ? 14 : 16} />
+ ) : (
+ <ChevronRight size={compact ? 14 : 16} />
)}
- </TableCell>
- )
- })}
+ </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>
)
- })
- ) : (
+ }
+
// ---------------------------------------------------
- // 3) 데이터가 없을 때
+ // 2) 일반 Row
+ // → "그룹핑된 컬럼"은 숨긴다
// ---------------------------------------------------
- <TableRow>
- <TableCell
- colSpan={table.getAllColumns().length}
- className={compactStyles.emptyRow + " text-center"}
+ return (
+ <TableRow
+ key={row.id}
+ className={compactStyles.row}
+ data-state={row.getIsSelected() && "selected"}
>
- No results.
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </UiTable>
- </div>
+ {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={{
+ ...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/common/permission-checker.tsx b/components/common/permission-checker.tsx
new file mode 100644
index 00000000..209e0022
--- /dev/null
+++ b/components/common/permission-checker.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useEffect } from "react";
+import { toast } from "sonner";
+import { usePathname } from "next/navigation";
+
+interface PermissionCheckerProps {
+ authorized: boolean;
+ message?: string;
+}
+
+export function PermissionChecker({ authorized, message }: PermissionCheckerProps) {
+ const pathname = usePathname();
+
+ useEffect(() => {
+ // Only show toast if authorization failed
+ if (!authorized) {
+ toast.error("Permission Denied", {
+ description: message || "You do not have permission to view this page. (Dev Mode: Viewing anyway)",
+ duration: 5000,
+ action: {
+ label: "Close",
+ onClick: () => toast.dismiss(),
+ },
+ });
+ } else {
+ // Optional: Show success toast only if explicitly needed,
+ // but usually we don't show toast for success to avoid noise.
+ // Uncomment for debugging:
+ toast.success("Authorized", { description: "Access granted.", duration: 1000 });
+ }
+ }, [authorized, message, pathname]);
+
+ return null;
+}
+
diff --git a/components/form-data-plant/delete-form-data-dialog.tsx b/components/form-data-plant/delete-form-data-dialog.tsx
index 6ac8f67c..2406407e 100644
--- a/components/form-data-plant/delete-form-data-dialog.tsx
+++ b/components/form-data-plant/delete-form-data-dialog.tsx
@@ -40,7 +40,8 @@ interface DeleteFormDataDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
formData: GenericData[]
formCode: string
- contractItemId: number
+ projectCode: string
+ packageCode: string
projectId?: number
showTrigger?: boolean
onSuccess?: () => void
@@ -50,7 +51,8 @@ interface DeleteFormDataDialogProps
export function DeleteFormDataDialog({
formData,
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
projectId,
showTrigger = true,
onSuccess,
@@ -77,7 +79,8 @@ export function DeleteFormDataDialog({
const result = await deleteFormDataByTags({
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
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 24b5827b..ba41a3c2 100644
--- a/components/form-data-plant/form-data-report-batch-dialog.tsx
+++ b/components/form-data-plant/form-data-report-batch-dialog.tsx
@@ -71,7 +71,8 @@ interface FormDataReportBatchDialogProps {
setOpen: Dispatch<SetStateAction<boolean>>;
columnsJSON: DataTableColumnJSON[];
reportData: ReportData[];
- packageId: number;
+ projectCode: string;
+ packageCode: string;
formId: number;
formCode: string;
}
@@ -81,7 +82,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
setOpen,
columnsJSON,
reportData,
- packageId,
+ projectCode,
+ packageCode,
formId,
formCode,
}) => {
@@ -100,8 +102,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null);
useEffect(() => {
- updateReportTempList(packageId, formId, setTempList);
- }, [packageId, formId]);
+ updateReportTempList(projectCode, packageCode, formId, setTempList);
+ }, [projectCode, packageCode, formId]);
const onClose = () => {
if (isUploading) {
@@ -361,7 +363,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<PublishDialog
open={publishDialogOpen}
onOpenChange={setPublishDialogOpen}
- packageId={packageId}
+ projectCode={projectCode}
+ packageCode={packageCode}
formCode={formCode}
fileBlob={generatedFileBlob || undefined}
/>
@@ -409,17 +412,19 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
};
type UpdateReportTempList = (
- packageId: number,
+ projectCode: string,
+ packageCode: string,
formId: number,
setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
) => void;
const updateReportTempList: UpdateReportTempList = async (
- packageId,
+ projectCode,
+ packageCode,
formId,
setTempList
) => {
- const tempList = await getReportTempList(packageId, formId);
+ const tempList = await getReportTempList(projectCode,packageCode, 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 9177ab36..2413fc28 100644
--- a/components/form-data-plant/form-data-report-dialog.tsx
+++ b/components/form-data-plant/form-data-report-dialog.tsx
@@ -49,7 +49,8 @@ interface FormDataReportDialogProps {
columnsJSON: DataTableColumnJSON[];
reportData: ReportData[];
setReportData: Dispatch<SetStateAction<ReportData[]>>;
- packageId: number;
+ projectCode: string;
+ packageCode: string;
formId: number;
formCode: string;
}
@@ -58,7 +59,8 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
columnsJSON,
reportData,
setReportData,
- packageId,
+ projectCode,
+ packageCode,
formId,
formCode,
}) => {
@@ -76,8 +78,8 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null);
useEffect(() => {
- updateReportTempList(packageId, formId, setTempList);
- }, [packageId, formId]);
+ updateReportTempList(projectCode, packageCode, formId, setTempList);
+ }, [projectCode,packageCode, formId]);
const onClose = async (value: boolean) => {
if (fileLoading) {
@@ -197,7 +199,8 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
<PublishDialog
open={publishDialogOpen}
onOpenChange={setPublishDialogOpen}
- packageId={packageId}
+ projectCode={projectCode}
+ packageCode={packageCode}
formCode={formCode}
fileBlob={generatedFileBlob || undefined}
/>
@@ -394,17 +397,19 @@ const importReportData: ImportReportData = async (
};
type UpdateReportTempList = (
- packageId: number,
+ projectCode: string,
+ packageCode: string,
formId: number,
setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
) => void;
const updateReportTempList: UpdateReportTempList = async (
- packageId,
+ projectCode,
+ packageCode,
formId,
setTempList
) => {
- const tempList = await getReportTempList(packageId, formId);
+ const tempList = await getReportTempList(projectCode,packageCode, 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 59ea6ade..66915198 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,7 +23,8 @@ interface FormDataReportTempUploadDialogProps {
columnsJSON: DataTableColumnJSON[];
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
- packageId: number;
+ projectCode: string;
+ packageCode: string;
formCode: string;
formId: number;
uploaderType: string;
@@ -35,7 +36,8 @@ export const FormDataReportTempUploadDialog: FC<
columnsJSON,
open,
setOpen,
- packageId,
+ projectCode,
+ packageCode,
formId,
formCode,
uploaderType,
@@ -83,14 +85,16 @@ export const FormDataReportTempUploadDialog: FC<
</div>
<TabsContent value="upload">
<FormDataReportTempUploadTab
- packageId={packageId}
+ projectCode={projectCode}
+ packageCode={packageCode}
formId={formId}
uploaderType={uploaderType}
/>
</TabsContent>
<TabsContent value="uploaded">
<FormDataReportTempUploadedListTab
- packageId={packageId}
+ projectCode={projectCode}
+ packageCode={packageCode}
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 81186ba4..41466f90 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,14 +36,15 @@ import { uploadReportTemp } from "@/lib/forms-plant/services";
const MAX_FILE_SIZE = 3000000;
interface FormDataReportTempUploadTabProps {
- packageId: number;
+ projectCode: string;
+ packageCode: string;
formId: number;
uploaderType: string;
}
export const FormDataReportTempUploadTab: FC<
FormDataReportTempUploadTabProps
-> = ({ packageId, formId, uploaderType }) => {
+> = ({ projectCode,packageCode, formId, uploaderType }) => {
const { toast } = useToast();
const params = useParams();
const lng = (params?.lng as string) || "ko";
@@ -94,7 +95,7 @@ export const FormDataReportTempUploadTab: FC<
formData.append("customFileName", file.name);
formData.append("uploaderType", uploaderType);
- await uploadReportTemp(packageId, formId, formData);
+ await uploadReportTemp(projectCode, packageCode, 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 4cfbad69..1b6cefaf 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,13 +39,14 @@ import { getReportTempList, deleteReportTempFile } from "@/lib/forms-plant/servi
import { VendorDataReportTemps } from "@/db/schema/vendorData";
interface FormDataReportTempUploadedListTabProps {
- packageId: number;
+ projectCode: string;
+ packageCode: string;
formId: number;
}
export const FormDataReportTempUploadedListTab: FC<
FormDataReportTempUploadedListTabProps
-> = ({ packageId, formId }) => {
+> = ({ projectCode,packageCode , formId }) => {
const params = useParams();
const lng = (params?.lng as string) || "ko";
const { t } = useTranslation(lng, "engineering");
@@ -57,12 +58,12 @@ export const FormDataReportTempUploadedListTab: FC<
useEffect(() => {
const getTempFiles = async () => {
- await updateReportTempList(packageId, formId, setPrevReportTemp);
+ await updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp);
setIsLoading(false);
};
getTempFiles();
- }, [packageId, formId]);
+ }, [projectCode,packageCode, formId]);
return (
<div>
@@ -70,7 +71,7 @@ export const FormDataReportTempUploadedListTab: FC<
<UploadedTempFiles
prevReportTemp={prevReportTemp}
updateReportTempList={() =>
- updateReportTempList(packageId, formId, setPrevReportTemp)
+ updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp)
}
isLoading={isLoading}
t={t}
@@ -80,17 +81,19 @@ export const FormDataReportTempUploadedListTab: FC<
};
type UpdateReportTempList = (
- packageId: number,
+ projectCode: string,
+ packageCode: string,
formId: number,
setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>>
) => Promise<void>;
const updateReportTempList: UpdateReportTempList = async (
- packageId,
+ projectCode,
+ packageCode,
formId,
setPrevReportTemp
) => {
- const tempList = await getReportTempList(packageId, formId);
+ const tempList = await getReportTempList(projectCode, packageCode, formId);
setPrevReportTemp(tempList);
};
diff --git a/components/form-data-plant/form-data-table.tsx b/components/form-data-plant/form-data-table.tsx
index 30c176bd..c6c79a69 100644
--- a/components/form-data-plant/form-data-table.tsx
+++ b/components/form-data-plant/form-data-table.tsx
@@ -76,7 +76,8 @@ interface GenericData {
export interface DynamicTableProps {
dataJSON: GenericData[];
columnsJSON: DataTableColumnJSON[];
- contractItemId: number;
+ projectCode: string;
+ packageCode: string;
formCode: string;
formId: number;
projectId: number;
@@ -89,7 +90,8 @@ export interface DynamicTableProps {
export default function DynamicTable({
dataJSON,
columnsJSON,
- contractItemId,
+ projectCode,
+ packageCode,
formCode,
formId,
projectId,
@@ -156,7 +158,8 @@ export default function DynamicTable({
// 서버 액션 호출
const result = await excludeFormDataByTags({
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
tagNumbers,
});
@@ -288,7 +291,7 @@ export default function DynamicTable({
try {
setIsLoadingStats(true);
// getFormStatusByVendor 서버 액션 직접 호출
- const data = await getFormStatusByVendor(projectId, contractItemId, formCode);
+ const data = await getFormStatusByVendor(projectId, projectCode, packageCode,formCode);
if (data && data.length > 0) {
setFormStats(data[0]);
@@ -339,9 +342,7 @@ export default function DynamicTable({
// SEDP compare dialog state
const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false);
- const [projectCode, setProjectCode] = React.useState<string>('');
- const [projectType, setProjectType] = React.useState<string>('plant');
- const [packageCode, setPackageCode] = React.useState<string>('');
+ const projectType = "plant";
// 새로 추가된 Template 다이얼로그 상태
const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false);
@@ -374,43 +375,13 @@ const [isLoadingRegisters, setIsLoadingRegisters] = React.useState(false);
React.useEffect(() => {
const getTempCount = async () => {
- const tempList = await getReportTempList(contractItemId, formId);
+ const tempList = await getReportTempList(projectCode, packageCode, formId);
setTempCount(tempList.length);
};
getTempCount();
- }, [contractItemId, formId, tempUpDialog]);
+ }, [projectCode,packageCode, 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(() => {
@@ -529,7 +500,7 @@ React.useEffect(() => {
async function handleSyncTags() {
try {
setIsSyncingTags(true);
- const result = await syncMissingTags(contractItemId, formCode);
+ const result = await syncMissingTags(projectCode,packageCode, formCode);
// Prepare the toast messages based on what changed
const changes = [];
@@ -562,9 +533,9 @@ React.useEffect(() => {
setIsLoadingTags(true);
// API 엔드포인트 호출 - 작업 시작만 요청
- const response = await fetch('/api/cron/form-tags/start', {
+ const response = await fetch('/api/cron/form-tags-plant/start', {
method: 'POST',
- body: JSON.stringify({ projectCode, formCode, contractItemId })
+ body: JSON.stringify({ projectCode, formCode, packageCode })
});
if (!response.ok) {
@@ -603,7 +574,7 @@ React.useEffect(() => {
// 5초마다 상태 확인
pollingRef.current = setInterval(async () => {
try {
- const response = await fetch(`/api/cron/form-tags/status?id=${id}`);
+ const response = await fetch(`/api/cron/form-tags-plant/status?id=${id}`);
if (!response.ok) {
throw new Error('Failed to get tag import status');
@@ -666,7 +637,8 @@ React.useEffect(() => {
tableData,
columnsJSON,
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
editableFieldsMap, // 추가: 편집 가능 필드 정보 전달
onPendingChange: setIsImporting, // Let importExcelData handle loading state
onDataUpdate: (newData) => {
@@ -747,7 +719,8 @@ React.useEffect(() => {
const sedpResult = await sendFormDataToSEDP(
formCode, // Send formCode instead of formName
projectId, // Project ID
- contractItemId,
+ projectCode,
+ packageCode,
tableData.filter(v=>v.status !== 'excluded'), // Table data
columnsJSON // Column definitions
);
@@ -1226,7 +1199,8 @@ React.useEffect(() => {
columns={columnsJSON}
rowData={rowAction?.row.original ?? null}
formCode={formCode}
- contractItemId={contractItemId}
+ projectCode={projectCode}
+ packageCode={packageCode}
editableFieldsMap={editableFieldsMap}
onUpdateSuccess={(updatedValues) => {
// Update the specific row in tableData when a single row is updated
@@ -1244,7 +1218,8 @@ React.useEffect(() => {
<DeleteFormDataDialog
formData={deleteTarget}
formCode={formCode}
- contractItemId={contractItemId}
+ projectCode={projectCode}
+ packageCode={packageCode}
projectId={projectId}
open={deleteDialogOpen}
onOpenChange={(open) => {
@@ -1257,16 +1232,6 @@ 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
@@ -1276,7 +1241,8 @@ React.useEffect(() => {
selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용
tableData={tableData} // SPR_LST_SETUP용 - 새로 추가
formCode={formCode}
- contractItemId={contractItemId}
+ projectCode={projectCode}
+ packageCode={packageCode}
editableFieldsMap={editableFieldsMap}
columnsJSON={columnsJSON}
onUpdateSuccess={(updatedValues) => {
@@ -1344,7 +1310,8 @@ React.useEffect(() => {
columnsJSON={columnsJSON}
open={tempUpDialog}
setOpen={setTempUpDialog}
- packageId={contractItemId}
+ projectCode={projectCode}
+ packageCode={packageCode}
formCode={formCode}
formId={formId}
uploaderType="vendor"
@@ -1356,7 +1323,8 @@ React.useEffect(() => {
columnsJSON={columnsJSON}
reportData={reportData}
setReportData={setReportData}
- packageId={contractItemId}
+ projectCode={projectCode}
+ packageCode={packageCode}
formCode={formCode}
formId={formId}
/>
@@ -1368,7 +1336,8 @@ React.useEffect(() => {
setOpen={setBatchDownDialog}
columnsJSON={columnsJSON}
reportData={selectedRowCount > 0 ? getSelectedRowsData() : tableData}
- packageId={contractItemId}
+ projectCode={projectCode}
+ packageCode={packageCode}
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 ffc6f2f9..8ac70c59 100644
--- a/components/form-data-plant/import-excel-form.tsx
+++ b/components/form-data-plant/import-excel-form.tsx
@@ -23,7 +23,8 @@ export interface ImportExcelOptions {
tableData: GenericData[];
columnsJSON: DataTableColumnJSON[];
formCode?: string;
- contractItemId?: number;
+ projectCode: string;
+ packageCode: string;
editableFieldsMap?: Map<string, string[]>; // 새로 추가
onPendingChange?: (isPending: boolean) => void;
onDataUpdate?: (updater: ((prev: GenericData[]) => GenericData[]) | GenericData[]) => void;
@@ -218,7 +219,8 @@ export async function importExcelData({
tableData,
columnsJSON,
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
editableFieldsMap = new Map(), // 새로 추가
onPendingChange,
onDataUpdate
@@ -527,14 +529,14 @@ export async function importExcelData({
}
});
- // If formCode and contractItemId are provided, save directly to DB
// importExcelData 함수에서 DB 저장 부분
- if (formCode && contractItemId) {
+ if (formCode && projectCode && packageCode) {
try {
// 배치 업데이트 함수 호출
const result = await updateFormDataBatchInDB(
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
importedData // 모든 imported rows를 한번에 전달
);
@@ -633,7 +635,6 @@ 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 a3a2ef0b..f63c2db8 100644
--- a/components/form-data-plant/publish-dialog.tsx
+++ b/components/form-data-plant/publish-dialog.tsx
@@ -37,19 +37,21 @@ import { Loader2, Check, ChevronsUpDown } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import {
- createRevisionAction,
- fetchDocumentsByPackageId,
- fetchStagesByDocumentId,
- fetchRevisionsByStageParams,
- Document,
- IssueStage,
- Revision
+ createSubmissionAction, // 새로운 액션 이름
+ fetchDocumentsByProjectAndPackage, // 업데이트된 액션
+ fetchStagesByDocumentIdPlant,
+ fetchSubmissionsByStageParams, // revisions 대신 submissions
} from "@/lib/vendor-document/service";
+import type {
+ StageDocument,
+ StageIssueStage,
+} from "@/db/schema/vendorDocu";
interface PublishDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
- packageId: number;
+ projectCode: string;
+ packageCode: string;
formCode: string;
fileBlob?: Blob;
}
@@ -57,7 +59,8 @@ interface PublishDialogProps {
export const PublishDialog: React.FC<PublishDialogProps> = ({
open,
onOpenChange,
- packageId,
+ projectCode,
+ packageCode,
formCode,
fileBlob,
}) => {
@@ -65,9 +68,10 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
const { data: session } = useSession();
// State for form data
- const [documents, setDocuments] = useState<Document[]>([]);
- const [stages, setStages] = useState<IssueStage[]>([]);
- const [latestRevision, setLatestRevision] = useState<string>("");
+ const [documents, setDocuments] = useState<StageDocument[]>([]);
+ const [stages, setStages] = useState<StageIssueStage[]>([]);
+ const [latestRevisionCode, setLatestRevisionCode] = useState<string>("");
+ const [latestRevisionNumber, setLatestRevisionNumber] = useState<number>(0);
// State for document search
const [openDocumentCombobox, setOpenDocumentCombobox] = useState(false);
@@ -77,9 +81,10 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
const [selectedDocId, setSelectedDocId] = useState<string>("");
const [selectedDocumentDisplay, setSelectedDocumentDisplay] = useState<string>("");
const [selectedStage, setSelectedStage] = useState<string>("");
- const [revisionInput, setRevisionInput] = useState<string>("");
- const [uploaderName, setUploaderName] = useState<string>("");
- const [comment, setComment] = useState<string>("");
+ const [revisionCodeInput, setRevisionCodeInput] = useState<string>("");
+ const [submitterName, setSubmitterName] = useState<string>("");
+ const [submissionTitle, setSubmissionTitle] = useState<string>("");
+ const [submissionDescription, setSubmissionDescription] = useState<string>("");
const [customFileName, setCustomFileName] = useState<string>(`${formCode}_document.docx`);
// Loading states
@@ -94,10 +99,10 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
)
: documents;
- // Set uploader name from session when dialog opens
+ // Set submitter name from session when dialog opens
useEffect(() => {
if (open && session?.user?.name) {
- setUploaderName(session.user.name);
+ setSubmitterName(session.user.name);
}
}, [open, session]);
@@ -107,24 +112,26 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
setSelectedDocId("");
setSelectedDocumentDisplay("");
setSelectedStage("");
- setRevisionInput("");
- // Only set uploaderName if not already set from session
- if (!session?.user?.name) setUploaderName("");
- setComment("");
- setLatestRevision("");
+ setRevisionCodeInput("");
+ setSubmissionTitle("");
+ setSubmissionDescription("");
+ // Only set submitterName if not already set from session
+ if (!session?.user?.name) setSubmitterName("");
+ setLatestRevisionCode("");
+ setLatestRevisionNumber(0);
setCustomFileName(`${formCode}_document.docx`);
setDocumentSearchValue("");
}
}, [open, formCode, session]);
- // Fetch documents based on packageId
+ // Fetch documents based on projectCode and packageCode
useEffect(() => {
async function loadDocuments() {
- if (packageId && open) {
+ if (projectCode && packageCode && open) {
setIsLoading(true);
try {
- const docs = await fetchDocumentsByPackageId(packageId);
+ const docs = await fetchDocumentsByProjectAndPackage(projectCode, packageCode);
setDocuments(docs);
} catch (error) {
console.error("Error fetching documents:", error);
@@ -136,7 +143,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
}
loadDocuments();
- }, [packageId, open]);
+ }, [projectCode, packageCode, open]);
// Fetch stages when document is selected
useEffect(() => {
@@ -146,11 +153,12 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
// Reset dependent fields
setSelectedStage("");
- setRevisionInput("");
- setLatestRevision("");
+ setRevisionCodeInput("");
+ setLatestRevisionCode("");
+ setLatestRevisionNumber(0);
try {
- const stagesList = await fetchStagesByDocumentId(parseInt(selectedDocId, 10));
+ const stagesList = await fetchStagesByDocumentIdPlant(parseInt(selectedDocId, 10));
setStages(stagesList);
} catch (error) {
console.error("Error fetching stages:", error);
@@ -166,65 +174,78 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
loadStages();
}, [selectedDocId]);
- // Fetch latest revision when stage is selected (for reference)
+ // Fetch latest submission (revision) when stage is selected
useEffect(() => {
- async function loadLatestRevision() {
+ async function loadLatestSubmission() {
if (selectedDocId && selectedStage) {
setIsLoading(true);
try {
- const revsList = await fetchRevisionsByStageParams(
+ const submissionsList = await fetchSubmissionsByStageParams(
parseInt(selectedDocId, 10),
selectedStage
);
- // 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 });
- });
+ // 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
+ );
- setLatestRevision(sortedRevisions[0].revision);
+ const latestSubmission = sortedSubmissions[0];
+ setLatestRevisionCode(latestSubmission.revisionCode);
+ setLatestRevisionNumber(latestSubmission.revisionNumber);
- // Pre-fill the revision input with an incremented value if possible
- if (sortedRevisions[0].revision.match(/^\d+$/)) {
+ // Auto-increment revision code
+ if (latestSubmission.revisionCode.match(/^\d+$/)) {
// If it's a number, increment it
- const nextRevision = String(parseInt(sortedRevisions[0].revision, 10) + 1);
- setRevisionInput(nextRevision);
- } else if (sortedRevisions[0].revision.match(/^[A-Za-z]$/)) {
+ const nextRevision = String(parseInt(latestSubmission.revisionCode, 10) + 1);
+ setRevisionCodeInput(nextRevision);
+ } else if (latestSubmission.revisionCode.match(/^[A-Za-z]$/)) {
// If it's a single letter, get the next letter
- const currentChar = sortedRevisions[0].revision.charCodeAt(0);
+ const currentChar = latestSubmission.revisionCode.charCodeAt(0);
const nextChar = String.fromCharCode(currentChar + 1);
- setRevisionInput(nextChar);
+ 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("");
+ }
} else {
// For other formats, just show the latest as reference
- setRevisionInput("");
+ setRevisionCodeInput("");
}
} else {
- // If no revisions exist, set default values
- setLatestRevision("");
- setRevisionInput("0");
+ // If no submissions exist, set default values
+ setLatestRevisionCode("");
+ setLatestRevisionNumber(0);
+ setRevisionCodeInput("Rev0"); // Start with Rev0
}
} catch (error) {
- console.error("Error fetching revisions:", error);
- toast.error("Failed to load revision information");
+ console.error("Error fetching submissions:", error);
+ toast.error("Failed to load submission information");
} finally {
setIsLoading(false);
}
} else {
- setLatestRevision("");
- setRevisionInput("");
+ setLatestRevisionCode("");
+ setLatestRevisionNumber(0);
+ setRevisionCodeInput("");
}
}
- loadLatestRevision();
+ loadLatestSubmission();
}, [selectedDocId, selectedStage]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!selectedDocId || !selectedStage || !revisionInput || !fileBlob) {
+ if (!selectedDocId || !selectedStage || !revisionCodeInput || !fileBlob) {
toast.error("Please fill in all required fields");
return;
}
@@ -235,17 +256,30 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
// Create FormData
const formData = new FormData();
formData.append("documentId", selectedDocId);
- formData.append("stage", selectedStage);
- formData.append("revision", revisionInput);
+ formData.append("stageName", selectedStage);
+ formData.append("revisionCode", revisionCodeInput);
formData.append("customFileName", customFileName);
- formData.append("uploaderType", "vendor"); // Default value
- if (uploaderName) {
- formData.append("uploaderName", uploaderName);
+ if (submitterName) {
+ formData.append("submittedBy", submitterName);
}
- if (comment) {
- formData.append("comment", comment);
+ 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));
}
// Append file as attachment
@@ -256,12 +290,14 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
formData.append("attachment", file);
}
- // Call server action directly
- const result = await createRevisionAction(formData);
+ // Call server action
+ const result = await createSubmissionAction(formData);
- if (result) {
+ if (result.success) {
toast.success("Document published successfully!");
onOpenChange(false);
+ } else {
+ toast.error(result.error || "Failed to publish document");
}
} catch (error) {
console.error("Error publishing document:", error);
@@ -301,7 +337,6 @@ 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
@@ -338,7 +373,6 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
: "opacity-0"
)}
/>
- {/* Add text-overflow handling for document items */}
<span className="truncate">{doc.docNumber} - {doc.title}</span>
</CommandItem>
))}
@@ -366,7 +400,6 @@ 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>
))}
@@ -375,28 +408,42 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
</div>
</div>
- {/* Revision Input */}
+ {/* Revision Code Input */}
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="revision" className="text-right">
+ <Label htmlFor="revisionCode" className="text-right">
Revision
</Label>
<div className="col-span-3">
<Input
- id="revision"
- value={revisionInput}
- onChange={(e) => setRevisionInput(e.target.value)}
- placeholder="Enter revision"
+ id="revisionCode"
+ value={revisionCodeInput}
+ onChange={(e) => setRevisionCodeInput(e.target.value)}
+ placeholder="Enter revision code (e.g., Rev0, A, 1)"
disabled={isLoading || !selectedStage}
/>
- {latestRevision && (
+ {latestRevisionCode && (
<p className="text-xs text-muted-foreground mt-1">
- Latest revision: {latestRevision}
+ Latest revision: {latestRevisionCode} (#{latestRevisionNumber})
</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>
@@ -411,16 +458,15 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
</div>
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="uploaderName" className="text-right">
- Uploader
+ <Label htmlFor="submitterName" className="text-right">
+ Submitter
</Label>
<div className="col-span-3">
<Input
- id="uploaderName"
- value={uploaderName}
- onChange={(e) => setUploaderName(e.target.value)}
+ id="submitterName"
+ value={submitterName}
+ onChange={(e) => setSubmitterName(e.target.value)}
placeholder="Your name"
- // Disable input but show a filled style
className={session?.user?.name ? "opacity-70" : ""}
readOnly={!!session?.user?.name}
/>
@@ -433,15 +479,15 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
</div>
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="comment" className="text-right">
- Comment
+ <Label htmlFor="description" className="text-right">
+ Description
</Label>
<div className="col-span-3">
<Textarea
- id="comment"
- value={comment}
- onChange={(e) => setComment(e.target.value)}
- placeholder="Optional comment"
+ id="description"
+ value={submissionDescription}
+ onChange={(e) => setSubmissionDescription(e.target.value)}
+ placeholder="Optional submission description"
className="resize-none"
/>
</div>
@@ -451,7 +497,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
<DialogFooter>
<Button
type="submit"
- disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionInput}
+ disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionCodeInput}
>
{isSubmitting ? (
<>
diff --git a/components/form-data-plant/spreadJS-dialog.tsx b/components/form-data-plant/spreadJS-dialog.tsx
index 2eb2c8ba..9f972676 100644
--- a/components/form-data-plant/spreadJS-dialog.tsx
+++ b/components/form-data-plant/spreadJS-dialog.tsx
@@ -92,7 +92,8 @@ interface TemplateViewDialogProps {
tableData?: GenericData[];
formCode: string;
columnsJSON: DataTableColumnJSON[]
- contractItemId: number;
+ projectCode: string;
+ packageCode: string;
editableFieldsMap?: Map<string, string[]>;
onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void;
}
@@ -142,7 +143,8 @@ export function TemplateViewDialog({
selectedRow,
tableData = [],
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
columnsJSON,
editableFieldsMap = new Map(),
onUpdateSuccess
@@ -1435,7 +1437,8 @@ export function TemplateViewDialog({
const { success, message } = await updateFormDataInDB(
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
dataToSave
);
@@ -1500,7 +1503,8 @@ export function TemplateViewDialog({
try {
const { success, message } = await updateFormDataInDB(
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
dataToSave
);
@@ -1551,7 +1555,8 @@ export function TemplateViewDialog({
selectedRow,
tableData,
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
onUpdateSuccess,
cellMappings,
columnsJSON,
diff --git a/components/form-data-plant/update-form-sheet.tsx b/components/form-data-plant/update-form-sheet.tsx
index bd75d8f3..b7f56f7e 100644
--- a/components/form-data-plant/update-form-sheet.tsx
+++ b/components/form-data-plant/update-form-sheet.tsx
@@ -65,7 +65,8 @@ interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef<typeof Shee
columns: DataTableColumnJSON[];
rowData: Record<string, any> | null;
formCode: string;
- contractItemId: number;
+ projectCode: string;
+ packageCode: string;
editableFieldsMap?: Map<string, string[]>; // 새로 추가
/** 업데이트 성공 시 호출될 콜백 */
onUpdateSuccess?: (updatedValues: Record<string, any>) => void;
@@ -77,7 +78,8 @@ export function UpdateTagSheet({
columns,
rowData,
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
editableFieldsMap = new Map(),
onUpdateSuccess,
...props
@@ -219,7 +221,8 @@ export function UpdateTagSheet({
const { success, message } = await updateFormDataInDB(
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
finalValues,
);
diff --git a/components/vendor-data-plant/project-swicher.tsx b/components/vendor-data-plant/project-swicher.tsx
index d3123709..9b8f9bea 100644
--- a/components/vendor-data-plant/project-swicher.tsx
+++ b/components/vendor-data-plant/project-swicher.tsx
@@ -1,6 +1,7 @@
"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 {
@@ -16,149 +17,103 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
-import { Check, ChevronsUpDown, Loader2 } from "lucide-react"
-interface ContractInfo {
- contractId: number
- contractName: string
+interface PackageData {
+ packageCode: string
+ packageName: string | null
}
-interface ProjectInfo {
+interface ProjectData {
projectId: number
projectCode: string
projectName: string
- contracts: ContractInfo[]
+ projectType: string
+ packages: PackageData[]
}
interface ProjectSwitcherProps {
isCollapsed: boolean
- projects: ProjectInfo[]
-
- // 상위가 관리하는 "현재 선택된 contractId"
- selectedContractId: number | null
-
- // 콜백: 사용자가 "어떤 contract"를 골랐는지
- // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함
- onSelectContract: (projectId: number, contractId: number) => void
-
- // 로딩 상태 (선택사항)
- isLoading?: boolean
+ projects: ProjectData[]
+ selectedProjectId: number
+ selectedPackageCode: string | null
+ onSelectPackage: (projectId: number, packageCode: string) => void
}
export function ProjectSwitcher({
isCollapsed,
projects,
- selectedContractId,
- onSelectContract,
- isLoading = false,
+ selectedProjectId,
+ selectedPackageCode,
+ onSelectPackage,
}: ProjectSwitcherProps) {
- const [popoverOpen, setPopoverOpen] = React.useState(false)
- const [searchTerm, setSearchTerm] = React.useState("")
+ const [open, setOpen] = React.useState(false)
- // 현재 선택된 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 selectedProject = projects.find(p => p.projectId === selectedProjectId)
+ const selectedPackage = selectedProject?.packages.find(
+ pkg => pkg.packageCode === selectedPackageCode
+ )
- // 검색어에 따른 필터링된 프로젝트/계약 목록
- 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])
- // 계약 선택 핸들러
- function handleSelectContract(projectId: number, contractId: number) {
- onSelectContract(projectId, contractId)
- setPopoverOpen(false)
- setSearchTerm("") // 검색어 초기화
- }
+ console.log(projects,"projects")
- // 총 계약 수 계산 (빈 상태 표시용)
- const totalContracts = filteredProjects.reduce((sum, project) => sum + project.contracts.length, 0)
+ const displayText = selectedPackage
+ ? `${selectedProject?.projectCode} - ${selectedPackage.packageCode}`
+ : selectedProject?.projectCode || "Select Package"
return (
- <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
+ <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
- type="button"
variant="outline"
- 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"
+ role="combobox"
+ aria-expanded={open}
+ aria-label="Select a package"
+ className={cn("w-full justify-between", isCollapsed && "w-[50px]")}
>
- {isLoading ? (
- <>
- <span className={cn(isCollapsed && "hidden")}>Loading...</span>
- <Loader2 className={cn("h-4 w-4 animate-spin", !isCollapsed && "ml-2")} />
- </>
+ {isCollapsed ? (
+ <ChevronsUpDown className="h-4 w-4" />
) : (
<>
- <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")} />
+ <span className="truncate">{displayText}</span>
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</>
)}
</Button>
</PopoverTrigger>
-
- <PopoverContent className="w-[320px] p-0" align="start">
+ <PopoverContent className="w-[300px] p-0">
<Command>
- <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) => (
+ <CommandInput placeholder="Search package..." />
+ <CommandList>
+ <CommandEmpty>No package found.</CommandEmpty>
+ {projects.map((project) => (
+ <CommandGroup key={project.projectId} heading={project.projectName}>
+ {project.packages.map((pkg) => (
<CommandItem
- key={contract.contractId}
- onSelect={() => handleSelectContract(project.projectId, contract.contractId)}
- value={`${project.projectName} ${contract.contractName}`}
- className="truncate"
- title={contract.contractName}
+ key={`${project.projectId}-${pkg.packageCode}`}
+ onSelect={() => {
+ onSelectPackage(project.projectId, pkg.packageCode)
+ setOpen(false)
+ }}
+ className="text-sm"
>
- <span className="truncate">{contract.contractName}</span>
<Check
className={cn(
- "ml-auto h-4 w-4 flex-shrink-0",
- selectedContractId === contract.contractId ? "opacity-100" : "opacity-0"
+ "mr-2 h-4 w-4",
+ selectedProjectId === project.projectId &&
+ selectedPackageCode === pkg.packageCode
+ ? "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 31ee6dc7..b746e69d 100644
--- a/components/vendor-data-plant/sidebar.tsx
+++ b/components/vendor-data-plant/sidebar.tsx
@@ -10,304 +10,265 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip"
-import { Package2, FormInput } from "lucide-react"
-import { useRouter, usePathname } from "next/navigation"
+import { List, FormInput, FileText } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"
-import { type FormInfo } from "@/lib/forms/services"
+import { getEngineeringForms, getIMForms } from "@/lib/tags-plant/service"
-interface PackageData {
- itemId: number
- itemName: string
+interface FormInfo {
+ formCode: string
+ formName: string
}
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
isCollapsed: boolean
- 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"
+ selectedPackageCode: string | null
+ selectedFormCode: string | null
+ currentMode: "master" | "engineering" | "im" | null
+ projectCode: string // 추가
+ onMasterTagListClick: () => void
+ onEngineeringFormClick: (formCode: string) => void
+ onIMFormClick: (formCode: string) => void
}
export function Sidebar({
className,
isCollapsed,
- packages,
- selectedPackageId,
- selectedProjectId,
- selectedContractId,
- onSelectPackage,
- forms,
- onSelectForm,
- isLoadingForms = false,
- mode = "IM",
+ selectedPackageCode,
+ selectedFormCode,
+ currentMode,
+ projectCode, // 추가
+ onMasterTagListClick,
+ onEngineeringFormClick,
+ onIMFormClick,
}: SidebarProps) {
- const router = useRouter()
- const rawPathname = usePathname()
- const pathname = rawPathname ?? ""
+ const [engineeringForms, setEngineeringForms] = React.useState<FormInfo[]>([])
+ const [imForms, setIMForms] = React.useState<FormInfo[]>([])
+ const [isLoadingEngineering, setIsLoadingEngineering] = React.useState(false)
+ const [isLoadingIM, setIsLoadingIM] = React.useState(false)
- /**
- * ---------------------------
- * 1) URL에서 현재 패키지 / 폼 코드 추출
- * ---------------------------
- */
- const segments = pathname.split("/").filter(Boolean)
-
- let currentItemId: number | null = null
- let currentFormCode: string | null = null
+ // Engineering 폼 로드
+ React.useEffect(() => {
+ if (!selectedPackageCode || !projectCode) {
+ setEngineeringForms([])
+ return
+ }
- const tagIndex = segments.indexOf("tag")
- if (tagIndex !== -1 && segments[tagIndex + 1]) {
- currentItemId = parseInt(segments[tagIndex + 1], 10)
- }
+ const loadEngineeringForms = async () => {
+ setIsLoadingEngineering(true)
+ try {
+ const result = await getEngineeringForms(projectCode, selectedPackageCode)
+ setEngineeringForms(result)
+ } catch (error) {
+ console.error("Engineering 폼 로딩 오류:", error)
+ setEngineeringForms([])
+ } finally {
+ setIsLoadingEngineering(false)
+ }
+ }
- const formIndex = segments.indexOf("form")
- if (formIndex !== -1) {
- const itemSegment = segments[formIndex + 1]
- const codeSegment = segments[formIndex + 2]
+ loadEngineeringForms()
+ }, [selectedPackageCode, projectCode])
- if (itemSegment) {
- currentItemId = parseInt(itemSegment, 10)
- }
- if (codeSegment) {
- currentFormCode = codeSegment
+ // IM 폼 로드
+ React.useEffect(() => {
+ if (!selectedPackageCode || !projectCode) {
+ setIMForms([])
+ return
}
- }
- /**
- * ---------------------------
- * 2) 패키지 클릭 핸들러 (IM 모드)
- * ---------------------------
- */
- const handlePackageClick = (itemId: number) => {
- // 상위 컴포넌트 상태 업데이트
- onSelectPackage(itemId)
-
- // 해당 태그 페이지로 라우팅
- // 예: /vendor-data-plant/tag/123
- const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
- router.push(`/${baseSegments}/tag/${itemId}`)
- }
+ const loadIMForms = async () => {
+ setIsLoadingIM(true)
+ try {
+ const result = await getIMForms(projectCode, selectedPackageCode)
+ setIMForms(result)
+ } catch (error) {
+ console.error("IM 폼 로딩 오류:", error)
+ setIMForms([])
+ } finally {
+ setIsLoadingIM(false)
+ }
+ }
- /**
- * ---------------------------
- * 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}`)
- }
+ loadIMForms()
+ }, [selectedPackageCode, projectCode])
- /**
- * ---------------------------
- * 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}`)
- }
+ const isMasterActive = currentMode === "master"
+ const isPackageSelected = selectedPackageCode !== null
return (
<div className={cn("pb-12", className)}>
<div className="space-y-4 py-4">
- {/* ---------- 패키지(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
+ {/* 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>
- 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 />
- </>
- )}
+ <Separator />
- {/* ---------- 폼 목록 (IM 모드) / 패키지와 폼 목록 (ENG 모드) ---------- */}
+ {/* Engineering Forms */}
<div className="py-1">
<h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed
- ? (mode === "IM" ? "F" : "P")
- : (mode === "IM" ? "Form Lists" : "Package Lists")
- }
+ {isCollapsed ? "E" : "Engineering"}
</h2>
- <ScrollArea className={cn(
- "px-1",
- mode === "IM" ? "h-[300px]" : "h-[450px]"
- )}>
+ <ScrollArea className="h-[250px] px-1">
<div className="space-y-1 p-2">
- {isLoadingForms ? (
+ {isLoadingEngineering ? (
Array.from({ length: 3 }).map((_, index) => (
- <div key={`form-skeleton-${index}`} className="px-2 py-1.5">
+ <div key={`eng-skeleton-${index}`} className="px-2 py-1.5">
<Skeleton className="h-8 w-full" />
</div>
))
- ) : 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
+ ) : !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
- 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" />
+ 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">
{form.formName}
- </Button>
- )
- })
- )
+ </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>
) : (
- // =========== 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
+ imForms.map((form) => {
+ const isActive =
+ currentMode === "im" &&
+ form.formCode === selectedFormCode
- 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>
- </>
+ 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"
)}
- </div>
- ))
- )
+ onClick={() => onIMFormClick(form.formCode)}
+ >
+ <FileText className="mr-2 h-4 w-4" />
+ {form.formName}
+ </Button>
+ )
+ })
)}
</div>
</ScrollArea>
diff --git a/components/vendor-data-plant/vendor-data-container.tsx b/components/vendor-data-plant/vendor-data-container.tsx
index 60ec2c94..7ce831df 100644
--- a/components/vendor-data-plant/vendor-data-container.tsx
+++ b/components/vendor-data-plant/vendor-data-container.tsx
@@ -4,28 +4,14 @@ 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, useSearchParams } from "next/navigation"
-import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services"
+import { usePathname, useRouter } from "next/navigation"
import { Separator } from "@/components/ui/separator"
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Button } from "@/components/ui/button"
-import { FormInput } from "lucide-react"
-import { Skeleton } from "@/components/ui/skeleton"
-import { selectedModeAtom } from '@/atoms'
-import { useAtom } from 'jotai'
+import { ProjectSwitcher } from "./project-swicher"
interface PackageData {
- itemId: number
- itemName: string
-}
-
-interface ContractData {
- contractId: number
- contractName: string
- packages: PackageData[]
+ packageCode: string
+ packageName: string | null
}
interface ProjectData {
@@ -33,7 +19,7 @@ interface ProjectData {
projectCode: string
projectName: string
projectType: string
- contracts: ContractData[]
+ packages: PackageData[]
}
interface VendorDataContainerProps {
@@ -44,18 +30,39 @@ interface VendorDataContainerProps {
children: React.ReactNode
}
-function getTagIdFromPathname(path: string | null): number | null {
- if (!path) return null;
+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 }
+ }
- // 태그 패턴 검사 (/tag/123)
- const tagMatch = path.match(/\/tag\/(\d+)/)
- if (tagMatch) return parseInt(tagMatch[1], 10)
+ const projectCode = segments[vendorDataIndex + 1] || null
+ const packageCode = segments[vendorDataIndex + 2] || null
- // 폼 패턴 검사 (/form/123/...)
- const formMatch = path.match(/\/form\/(\d+)/)
- if (formMatch) return parseInt(formMatch[1], 10)
+ // /eng/{formCode} 또는 /im/{formCode} 패턴 체크
+ const modeSegment = segments[vendorDataIndex + 3]
+ const formCode = segments[vendorDataIndex + 4] || null
- return null
+ 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 }
}
export function VendorDataContainer({
@@ -67,267 +74,106 @@ 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 [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 [selectedPackageCode, setSelectedPackageCode] = React.useState<string | null>(null)
const [selectedFormCode, setSelectedFormCode] = React.useState<string | null>(null)
- const [isLoadingForms, setIsLoadingForms] = React.useState(false)
+ const [currentMode, setCurrentMode] = React.useState<"master" | "engineering" | "im" | null>(null)
- 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에서 모드가 변경되면 상태도 업데이트 (ship 프로젝트가 아닐 때만)
+ // URL 변경 시 상태 동기화
React.useEffect(() => {
- if (!isShipProject) {
- const modeFromUrl = searchParams?.get('mode')
- if (modeFromUrl === "ENG" || modeFromUrl === "IM") {
- setSelectedMode(modeFromUrl)
+ 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)
}
}
- }, [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 (formCode) {
+ setSelectedFormCode(formCode)
+ } else {
+ setSelectedFormCode(null)
}
- }, [isShipProject, router])
-
- // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅
- React.useEffect(() => {
- if (!currentContract) return
-
- if (tagIdNumber) {
- setSelectedPackageId(tagIdNumber)
+
+ if (mode) {
+ setCurrentMode(mode)
} else {
- // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로
- if (currentContract.packages?.length) {
- setSelectedPackageId(currentContract.packages[0].itemId)
- } else {
- setSelectedPackageId(null)
- }
+ setCurrentMode(null)
}
- }, [tagIdNumber, currentContract])
-
- // (2) 프로젝트 변경 시 계약 초기화
- // React.useEffect(() => {
- // if (currentProject?.contracts.length) {
- // setSelectedContractId(currentProject.contracts[0].contractId)
- // } else {
- // setSelectedContractId(0)
- // }
- // }, [currentProject])
+ }, [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("/")
+ }
- // (3) 패키지 ID와 모드가 변경될 때마다 폼 로딩
- React.useEffect(() => {
- const packageId = getTagIdFromPathname(pathname)
+ // 프로젝트 및 패키지 선택 핸들러
+ const handleSelectPackage = (projectId: number, packageCode: string) => {
+ const project = projects.find(p => p.projectId === projectId)
+ if (!project) return
- if (packageId) {
- setSelectedPackageId(packageId)
-
- // URL에서 패키지 ID를 얻었을 때 즉시 폼 로드
- loadFormsList(packageId, selectedMode);
- } else if (currentContract?.packages?.length) {
- const firstPackageId = currentContract.packages[0].itemId;
- setSelectedPackageId(firstPackageId);
- loadFormsList(firstPackageId, selectedMode);
- }
- }, [pathname, currentContract, selectedMode])
-
- // 모드에 따른 폼 로드 함수
- const loadFormsList = async (packageId: number, mode: "IM" | "ENG") => {
- if (!packageId) return;
+ setSelectedProjectId(projectId)
+ setSelectedPackageCode(packageCode)
+ setSelectedFormCode(null)
+ setCurrentMode("master")
- 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)
+ const baseUrl = getBaseUrl()
+ router.push(`${baseUrl}/${project.projectCode}/${packageCode}`)
+ }
- if (selectedContract?.packages?.length) {
- const firstPackageId = selectedContract.packages[0].itemId
- setSelectedPackageId(firstPackageId)
+ // Master Tag List 클릭 핸들러
+ const handleMasterTagListClick = () => {
+ if (!selectedPackageCode) return
- // 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([])
+ const project = projects.find(p => p.projectId === selectedProjectId)
+ if (!project) return
+
+ setCurrentMode("master")
setSelectedFormCode(null)
- setSelectedMode("ENG")
- }
-}
-
- function handleSelectPackage(itemId: number) {
- setSelectedPackageId(itemId)
- }
-
- function handleSelectForm(formName: string) {
- const form = formList.find((f) => f.formName === formName)
- if (form) {
- setSelectedFormCode(form.formCode)
- }
+
+ const baseUrl = getBaseUrl()
+ router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}`)
}
-
- // 모드 변경 핸들러
-// 모드 변경 핸들러
-const handleModeChange = async (mode: "IM" | "ENG") => {
- // ship 프로젝트인 경우 모드 변경 금지
- if (isShipProject && mode !== "ENG") return;
- setSelectedMode(mode);
+ // 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}`)
+ }
- // 모드가 변경될 때 자동 네비게이션
- if (currentContract?.packages?.length) {
- const firstPackageId = currentContract.packages[0].itemId;
+ // IM 폼 클릭 핸들러
+ const handleIMFormClick = (formCode: string) => {
+ if (!selectedPackageCode) return
- 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);
+ const project = projects.find(p => p.projectId === selectedProjectId)
+ if (!project) return
+
+ setCurrentMode("im")
+ setSelectedFormCode(formCode)
+
+ const baseUrl = getBaseUrl()
+ router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/im/${formCode}`)
}
-};
return (
<TooltipProvider delayDuration={0}>
@@ -351,151 +197,28 @@ const handleModeChange = async (mode: "IM" | "ENG") => {
<ProjectSwitcher
isCollapsed={isCollapsed}
projects={projects}
- selectedContractId={selectedContractId}
- onSelectContract={handleSelectContract}
+ selectedProjectId={selectedProjectId}
+ selectedPackageCode={selectedPackageCode}
+ onSelectPackage={handleSelectPackage}
/>
</div>
<Separator />
- {!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"
- />
- </>
- )}
+ <Sidebar
+ isCollapsed={isCollapsed}
+ selectedPackageCode={selectedPackageCode}
+ selectedFormCode={selectedFormCode}
+ currentMode={currentMode}
+ onMasterTagListClick={handleMasterTagListClick}
+ onEngineeringFormClick={handleEngineeringFormClick}
+ onIMFormClick={handleIMFormClick}
+ />
</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>