From 5ca88c4869be338f4b0e506a679e4dc4e029d7aa Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 27 Nov 2025 18:26:06 +0900 Subject: (김준회) dolce v2로 전환, 미사용 라우터들 비활성화 처리 (prefix: _) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../partners/(partners)/_document-upload/page.tsx | 154 +++++ .../_dolce-upload-v3/dolce-upload-page-v3.tsx | 590 +++++++++++++++++ .../partners/(partners)/_dolce-upload-v3/page.tsx | 49 ++ .../(partners)/_dolce-upload/dolce-upload-page.tsx | 420 ++++++++++++ .../partners/(partners)/_dolce-upload/page.tsx | 67 ++ .../(partners)/document-list-ship-legacy/page.tsx | 145 +++++ .../document-list-ship/dolce-upload-page-v2.tsx | 718 +++++++++++++++++++++ .../(partners)/document-list-ship/page.tsx | 200 ++---- .../partners/(partners)/document-upload/page.tsx | 154 ----- .../dolce-upload-v2/dolce-upload-page-v2.tsx | 718 --------------------- .../partners/(partners)/dolce-upload-v2/page.tsx | 69 -- .../dolce-upload-v3/dolce-upload-page-v3.tsx | 590 ----------------- .../partners/(partners)/dolce-upload-v3/page.tsx | 49 -- .../(partners)/dolce-upload/dolce-upload-page.tsx | 420 ------------ .../partners/(partners)/dolce-upload/page.tsx | 67 -- 15 files changed, 2205 insertions(+), 2205 deletions(-) create mode 100644 app/[lng]/partners/(partners)/_document-upload/page.tsx create mode 100644 app/[lng]/partners/(partners)/_dolce-upload-v3/dolce-upload-page-v3.tsx create mode 100644 app/[lng]/partners/(partners)/_dolce-upload-v3/page.tsx create mode 100644 app/[lng]/partners/(partners)/_dolce-upload/dolce-upload-page.tsx create mode 100644 app/[lng]/partners/(partners)/_dolce-upload/page.tsx create mode 100644 app/[lng]/partners/(partners)/document-list-ship-legacy/page.tsx create mode 100644 app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx delete mode 100644 app/[lng]/partners/(partners)/document-upload/page.tsx delete mode 100644 app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx delete mode 100644 app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx delete mode 100644 app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx delete mode 100644 app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx delete mode 100644 app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx delete mode 100644 app/[lng]/partners/(partners)/dolce-upload/page.tsx diff --git a/app/[lng]/partners/(partners)/_document-upload/page.tsx b/app/[lng]/partners/(partners)/_document-upload/page.tsx new file mode 100644 index 00000000..c67a603c --- /dev/null +++ b/app/[lng]/partners/(partners)/_document-upload/page.tsx @@ -0,0 +1,154 @@ +// app/(vendor)/stage-submissions/page.tsx +import * as React from "react" +import { searchParamsCache } from "@/lib/vendor-document-list/plant/upload/validation" +import { StageSubmissionsTable } from "@/lib/vendor-document-list/plant/upload/table" +import { redirect } from "next/navigation" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { getServerSession } from 'next-auth/next' +import { authOptions } from '@/app/api/auth/[...nextauth]/route' +import { getStageSubmissions, getProjects, getSubmissionStats } from "@/lib/vendor-document-list/plant/upload/service" +import db from "@/db/db" +import { eq } from "drizzle-orm" +import { vendors } from "@/db/schema" + +export default async function StageSubmissionsPage({ + searchParams, +}: { + searchParams: Promise> +}) { + // Session 체크 + const session = await getServerSession(authOptions) + + if (!session?.user?.companyId) { + redirect("/partners") + } + + + const vendor = await db.query.vendors.findFirst({ + where: eq(vendors.id, session.user.companyId), + columns: { + vendorName: true, + vendorCode: true, + } + }) + + const params = searchParamsCache.parse(await searchParams) + + const submissionsPromise = getStageSubmissions(params) + const projectsPromise = getProjects() + const statsPromise = getSubmissionStats() + + const [submissions, projects, stats] = await Promise.all([ + submissionsPromise, + projectsPromise, + statsPromise + ]) + + return ( +
+ {/* Header */} +
+
+

My Stage Submissions

+

+ Manage document submissions for your approved stages (SWP) +

+
+
+ {/* */} + {/* Company: */} + {vendor.vendorName || "Your Company"} + +

+ Buyer Approved Documents +

+ {/*
*/} +
+
+ + {/* Stats Cards */} +
+ + + Pending Submissions + + +
+ {stats.pending} +
+
+
+ + + + Overdue + + +
+ {stats.overdue} +
+
+
+ + + + Awaiting Sync + + +
+ {stats.awaitingSync} +
+
+
+ + + + Completed + + +
+ {stats.completed} +
+
+
+
+ + {/* Main Table */} + + + Your Submission List + + View and manage document submissions for your company's stages + + + + +
+ Loading your submissions... +
+
+ } + > + ({ id: p.id, code: p.code || "" })) } + ])} + selectedProjectId={params.projectId} + + /> + + + + + ) +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/_dolce-upload-v3/dolce-upload-page-v3.tsx b/app/[lng]/partners/(partners)/_dolce-upload-v3/dolce-upload-page-v3.tsx new file mode 100644 index 00000000..55bedb38 --- /dev/null +++ b/app/[lng]/partners/(partners)/_dolce-upload-v3/dolce-upload-page-v3.tsx @@ -0,0 +1,590 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useParams } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { InfoIcon, RefreshCw, Search, Upload, Plus, Cloud } from "lucide-react"; +import { toast } from "sonner"; +import { useTranslation } from "@/i18n/client"; +import { + UnifiedDwgReceiptItem, + DetailDwgReceiptItem, + FileInfoItem, + fetchDwgReceiptList, + getVendorSessionInfo, + fetchVendorProjects, + fetchDetailDwgReceiptListV2, + fetchFileInfoListV2, + fetchPendingSyncItems, + deleteLocalFile, +} from "@/lib/dolce-v2/actions"; +import { DrawingListTableV2 } from "@/lib/dolce/table/drawing-list-table-v2"; +import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; +import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; +import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns"; +import { createFileListColumns } from "@/lib/dolce/table/file-list-columns"; + +// 다이얼로그 (V2/V3) +import { B4BulkUploadDialogV3Sync } from "@/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3"; +import { AddAndModifyDetailDrawingDialogV2 } from "@/lib/dolce-v2/dialogs/add-and-modify-detail-drawing-dialog-v2"; +import { UploadFilesToDetailDialogV2 } from "@/lib/dolce-v2/dialogs/upload-files-to-detail-dialog-v2"; +import { SyncItemsDialog } from "@/lib/dolce-v2/dialogs/sync-items-dialog"; + +interface DolceUploadPageV3Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default function DolceUploadPageV3({ searchParams }: DolceUploadPageV3Props) { + const params = useParams(); + const lng = params?.lng as string; + const { t } = useTranslation(lng, "dolce"); + + // URL에서 초기 프로젝트 코드 + const initialProjNo = (searchParams.projNo as string) || ""; + + // 상태 관리 + const [drawings, setDrawings] = useState([]); + const [projects, setProjects] = useState>([]); + const [vendorInfo, setVendorInfo] = useState<{ + userId: string; + userName: string; + email: string; + vendorCode: string; + vendorName: string; + drawingKind: "B3" | "B4"; + } | null>(null); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + + // 필터 상태 + const [projNo, setProjNo] = useState(initialProjNo); + const [drawingNo, setDrawingNo] = useState(""); + const [drawingName, setDrawingName] = useState(""); + const [discipline, setDiscipline] = useState(""); + const [manager, setManager] = useState(""); + const [documentType, setDocumentType] = useState("ALL"); // B4 전용 + + // 선택된 도면 및 상세도면 + const [selectedDrawing, setSelectedDrawing] = useState(null); + const [detailDrawings, setDetailDrawings] = useState([]); + const [selectedDetail, setSelectedDetail] = useState(null); + const [files, setFiles] = useState([]); + const [isLoadingDetails, setIsLoadingDetails] = useState(false); + const [, setIsLoadingFiles] = useState(false); + const [downloadingFileId, setDownloadingFileId] = useState(null); + + // 다이얼로그 상태 + const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false); + const [syncDialogOpen, setSyncDialogOpen] = useState(false); + + // 동기화 상태 + const [pendingSyncCount, setPendingSyncCount] = useState(0); + + // 미동기화 건수 확인 + const checkPendingSync = useCallback(async () => { + if (!projNo || !vendorInfo) return; + try { + const items = await fetchPendingSyncItems({ projectNo: projNo, userId: vendorInfo.userId }); + setPendingSyncCount(items.length); + } catch (e) { + console.error("Failed to check pending sync items", e); + } + }, [projNo, vendorInfo]); + + // 초기 데이터 로드 + const loadInitialData = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + + const [vendorInfoData, projectsData] = await Promise.all([ + getVendorSessionInfo(), + fetchVendorProjects(), + ]); + + setVendorInfo(vendorInfoData as typeof vendorInfo); + setProjects(projectsData); + + if (initialProjNo) { + const drawingsData = await fetchDwgReceiptList({ + project: initialProjNo, + drawingKind: vendorInfoData.drawingKind, + drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "", + }); + setDrawings(drawingsData); + } + } catch (err) { + console.error("초기 데이터 로드 실패:", err); + setError(err instanceof Error ? err.message : t("page.initialLoadError")); + toast.error(t("page.initialLoadError")); + } finally { + setIsLoading(false); + } + }, [initialProjNo, t]); + + // 도면 목록 조회 + const loadDrawings = useCallback(async () => { + if (!projNo || !vendorInfo) return; + + try { + setIsRefreshing(true); + setError(null); + + const drawingsData = await fetchDwgReceiptList({ + project: projNo, + drawingKind: vendorInfo.drawingKind, + drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "", + }); + + setDrawings(drawingsData); + toast.success(t("page.drawingLoadSuccess")); + + // 동기화 상태 체크 + checkPendingSync(); + + } catch (err) { + console.error("도면 로드 실패:", err); + setError(err instanceof Error ? err.message : t("page.drawingLoadError")); + toast.error(t("page.drawingLoadError")); + } finally { + setIsRefreshing(false); + } + }, [projNo, vendorInfo, t, checkPendingSync]); + + // 상세도면 목록 로드 (V2 API 사용) + const loadDetailDrawings = useCallback(async () => { + if (!selectedDrawing) { + setDetailDrawings([]); + setSelectedDetail(null); + return; + } + + try { + setIsLoadingDetails(true); + // V2: 로컬 임시 저장 건 포함 조회 + const data = await fetchDetailDwgReceiptListV2({ + project: selectedDrawing.ProjectNo, + drawingNo: selectedDrawing.DrawingNo, + discipline: selectedDrawing.Discipline, + drawingKind: selectedDrawing.DrawingKind, + userId: vendorInfo?.userId || "", + }); + setDetailDrawings(data); + + if (data.length > 0) { + setSelectedDetail(data[0]); + } else { + setSelectedDetail(null); + } + + // 동기화 상태 체크 + checkPendingSync(); + + } catch (error) { + console.error("상세도면 로드 실패:", error); + toast.error(t("detailDialog.detailLoadError")); + setDetailDrawings([]); + setSelectedDetail(null); + } finally { + setIsLoadingDetails(false); + } + }, [selectedDrawing, vendorInfo, t, checkPendingSync]); + + // 파일 목록 로드 (V2 API 사용) + const loadFiles = useCallback(async () => { + if (!selectedDetail) { + setFiles([]); + return; + } + + try { + setIsLoadingFiles(true); + // V2: 로컬 임시 파일 포함 조회 + const data = await fetchFileInfoListV2(selectedDetail.UploadId); + setFiles(data); + + // 동기화 상태 체크 + checkPendingSync(); + + } catch (error) { + console.error("파일 목록 로드 실패:", error); + toast.error(t("detailDialog.fileLoadError")); + setFiles([]); + } finally { + setIsLoadingFiles(false); + } + }, [selectedDetail, t, checkPendingSync]); + + // 동기화 완료 핸들러 + const handleSyncComplete = useCallback(() => { + checkPendingSync(); + if (selectedDrawing) loadDetailDrawings(); + if (selectedDetail) loadFiles(); + }, [checkPendingSync, selectedDrawing, selectedDetail, loadDetailDrawings, loadFiles]); + + // 초기 데이터 로드 + useEffect(() => { + loadInitialData(); + }, [loadInitialData]); + + // 프로젝트 변경 시 자동 검색 + useEffect(() => { + if (projNo && vendorInfo) { + loadDrawings(); + } + }, [projNo, vendorInfo, loadDrawings]); + + // 선택된 도면 변경 시 상세도면 로드 + useEffect(() => { + loadDetailDrawings(); + }, [selectedDrawing, loadDetailDrawings]); + + // 선택된 상세도면 변경 시 파일 목록 로드 + useEffect(() => { + loadFiles(); + }, [selectedDetail, loadFiles]); + + const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => { + setSelectedDrawing(drawing); + }; + + const handleSearch = () => { + loadDrawings(); + }; + + const handleRefresh = () => { + loadDrawings(); + }; + + const handleRefreshDetails = () => { + loadDetailDrawings(); + }; + + // 완료 핸들러들 + const handleBulkUploadComplete = () => { + loadDrawings(); + checkPendingSync(); + }; + const handleAddComplete = () => { + setAddDialogOpen(false); + loadDetailDrawings(); + checkPendingSync(); + }; + const handleUploadComplete = () => { + setUploadFilesDialogOpen(false); + loadFiles(); + checkPendingSync(); + }; + + const handleDeleteFile = async (file: FileInfoItem) => { + if (!confirm(lng === "ko" ? "정말로 파일을 삭제하시겠습니까?" : "Are you sure you want to delete this file?")) return; + + try { + const result = await deleteLocalFile(file.FileId); + if (result.success) { + toast.success(lng === "ko" ? "파일이 삭제되었습니다." : "File deleted."); + loadFiles(); + checkPendingSync(); + } else { + throw new Error(result.error); + } + } catch (e) { + console.error("File delete failed", e); + toast.error(lng === "ko" ? "파일 삭제 실패" : "File delete failed"); + } + }; + + const handleDownload = async (file: FileInfoItem) => { + try { + setDownloadingFileId(file.FileId); + toast.info(t("detailDialog.downloadPreparing")); + const response = await fetch("/api/dolce/download", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fileId: file.FileId, + userId: file.CreateUserId, + fileName: file.FileName, + }), + }); + + if (!response.ok) throw new Error(t("detailDialog.downloadError")); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = file.FileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + toast.success(t("detailDialog.downloadSuccess")); + } catch (error) { + console.error("파일 다운로드 실패:", error); + toast.error(t("detailDialog.downloadError")); + } finally { + setDownloadingFileId(null); + } + }; + + // 필터 로직 + const filteredDrawings = useMemo(() => { + let result = drawings.filter((drawing) => { + if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) return false; + if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) return false; + if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) return false; + if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) && + !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) return false; + return true; + }); + + if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") { + result = result.filter((drawing) => { + if (drawing.DrawingKind !== "B4") return false; + const gttDrawing = drawing as { DrawingMoveGbn?: string }; + if (documentType === "SHI_INPUT") return gttDrawing.DrawingMoveGbn === "도면제출"; + else if (documentType === "GTT_DELIVERABLES") return gttDrawing.DrawingMoveGbn === "도면입수"; + return true; + }); + } + return result; + }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]); + + const getDetailDrawingId = (detail: DetailDwgReceiptItem) => `${detail.RegisterId}_${detail.UploadId}`; + const getDrawingId = (drawing: UnifiedDwgReceiptItem) => `${drawing.ProjectNo}_${drawing.DrawingNo}_${drawing.Discipline}`; + + const canAddDetailDrawing = vendorInfo && ( + vendorInfo.drawingKind === "B3" || + (vendorInfo.drawingKind === "B4" && selectedDrawing && 'DrawingMoveGbn' in selectedDrawing && selectedDrawing.DrawingMoveGbn === "도면입수") + ); + + // 파일 리스트 컬럼 정의 + const fileColumns = createFileListColumns({ + onDownload: handleDownload, + onDelete: handleDeleteFile, + lng, + downloadingFileId + }); + + if (isLoading) { + return ( +
+ + +
+ ); + } + + return ( +
+ {error && {error}} + + {/* 헤더 및 Sync 컨트롤 */} +
+
+

+ Dolce Upload V3 Sync Enabled +

+
+
+ +
+
+ + {!projNo && {t("page.selectProject")}} + + {/* 필터 카드 */} + + {t("filter.title")} + +
+
+ + +
+ {/* 기타 필터들 */} +
setDrawingNo(e.target.value)} />
+
setDrawingName(e.target.value)} />
+
setDiscipline(e.target.value)} />
+
setManager(e.target.value)} />
+ {vendorInfo?.drawingKind === "B4" && ( +
+ + +
+ )} +
+
+ + {vendorInfo?.drawingKind === "B4" && ( + + )} + +
+
+
+ + {/* 메인 컨텐츠 영역 */} + + {t("drawingList.title")} + + + + + +
+ + + {t("detailDialog.detailListTitle")} +
+ + {canAddDetailDrawing && } +
+
+ + + +
+ + + + {t("detailDialog.fileListTitle")} + {selectedDetail && canAddDetailDrawing && ( + + )} + + + + + +
+ + {/* 다이얼로그 영역 */} + {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( + + )} + + {vendorInfo && selectedDrawing && ( + + )} + + {vendorInfo && selectedDetail && ( + + )} + + {/* 동기화 다이얼로그 */} + {vendorInfo && projNo && ( + + )} +
+ ); +} diff --git a/app/[lng]/partners/(partners)/_dolce-upload-v3/page.tsx b/app/[lng]/partners/(partners)/_dolce-upload-v3/page.tsx new file mode 100644 index 00000000..f62f486b --- /dev/null +++ b/app/[lng]/partners/(partners)/_dolce-upload-v3/page.tsx @@ -0,0 +1,49 @@ +import { Suspense } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import DolceUploadPageV3 from "./dolce-upload-page-v3"; +import { Shell } from "@/components/shell"; + +export const metadata = { + title: "조선 벤더문서 업로드(DOLCE) V3", + description: "조선 설계문서 업로드 및 관리 - 오프라인 동기화 지원", +}; + +function DolceUploadSkeleton() { + return ( +
+ + +
+ ); +} + +export default async function DolceUploadPageWrapper({ + params, + searchParams, +}: { + params: Promise<{ lng: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const { lng } = await params; + const resolvedParams = await searchParams; + + return ( + +
+
+

+ {lng === "ko" ? "DOLCE 도면 업로드 V3 (동기화)" : "DOLCE Drawing Upload V3 (Sync)"} +

+

+ {lng === "ko" ? "임시 저장 및 서버 동기화 기능을 지원합니다." : "Supports temporary save and server synchronization."} +

+
+
+ + }> + + +
+ ); +} diff --git a/app/[lng]/partners/(partners)/_dolce-upload/dolce-upload-page.tsx b/app/[lng]/partners/(partners)/_dolce-upload/dolce-upload-page.tsx new file mode 100644 index 00000000..1bb876fb --- /dev/null +++ b/app/[lng]/partners/(partners)/_dolce-upload/dolce-upload-page.tsx @@ -0,0 +1,420 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useParams } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { InfoIcon, RefreshCw, Search, Upload } from "lucide-react"; +import { toast } from "sonner"; +import { useTranslation } from "@/i18n/client"; +import { + UnifiedDwgReceiptItem, + fetchDwgReceiptList, + getVendorSessionInfo, + fetchVendorProjects, +} from "@/lib/dolce/actions"; +import { DrawingListTable } from "@/lib/dolce/table/drawing-list-table"; +import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; +import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; +import { DetailDrawingDialog } from "@/lib/dolce/dialogs/detail-drawing-dialog"; +// V2: MatchBatchFileDwg+MatchBatchFileDwgEdit API 사용, 별도의 RegisterKind 선택 없이 결과값 기준으로 업로드 +import { B4BulkUploadDialogV3 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v3"; +// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 +// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; + +interface DolceUploadPageProps { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default function DolceUploadPage({ searchParams }: DolceUploadPageProps) { + const params = useParams(); + const lng = params?.lng as string; + const { t } = useTranslation(lng, "dolce"); + + // URL에서 초기 프로젝트 코드 + const initialProjNo = (searchParams.projNo as string) || ""; + + // 상태 관리 + const [drawings, setDrawings] = useState([]); + const [projects, setProjects] = useState>([]); + const [vendorInfo, setVendorInfo] = useState<{ + userId: string; + userName: string; + email: string; + vendorCode: string; + vendorName: string; + drawingKind: "B3" | "B4"; + } | null>(null); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + + // 필터 상태 + const [projNo, setProjNo] = useState(initialProjNo); + const [drawingNo, setDrawingNo] = useState(""); + const [drawingName, setDrawingName] = useState(""); + const [discipline, setDiscipline] = useState(""); + const [manager, setManager] = useState(""); + const [documentType, setDocumentType] = useState("ALL"); // B4 전용 + + // 선택된 도면 (다이얼로그용) + const [selectedDrawing, setSelectedDrawing] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + + // 일괄 업로드 다이얼로그 + const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); + + // 초기 데이터 로드 + const loadInitialData = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + + // 병렬로 데이터 로드 + const [vendorInfoData, projectsData] = await Promise.all([ + getVendorSessionInfo(), + fetchVendorProjects(), + ]); + + setVendorInfo(vendorInfoData as typeof vendorInfo); + setProjects(projectsData); + + // 초기 프로젝트가 있으면 도면 로드 + if (initialProjNo) { + const drawingsData = await fetchDwgReceiptList({ + project: initialProjNo, + drawingKind: vendorInfoData.drawingKind, + drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "", + }); + setDrawings(drawingsData); + } + } catch (err) { + console.error("초기 데이터 로드 실패:", err); + setError(err instanceof Error ? err.message : t("page.initialLoadError")); + toast.error(t("page.initialLoadError")); + } finally { + setIsLoading(false); + } + }, [initialProjNo, t]); + + // 도면 목록 조회 + const loadDrawings = useCallback(async () => { + if (!projNo || !vendorInfo) return; + + try { + setIsRefreshing(true); + setError(null); + + const drawingsData = await fetchDwgReceiptList({ + project: projNo, + drawingKind: vendorInfo.drawingKind, + drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "", + }); + + setDrawings(drawingsData); + toast.success(t("page.drawingLoadSuccess")); + } catch (err) { + console.error("도면 로드 실패:", err); + setError(err instanceof Error ? err.message : t("page.drawingLoadError")); + toast.error(t("page.drawingLoadError")); + } finally { + setIsRefreshing(false); + } + }, [projNo, vendorInfo, t]); + + // 초기 데이터 로드 + useEffect(() => { + loadInitialData(); + }, [loadInitialData]); + + // 프로젝트 변경 시 자동 검색 + useEffect(() => { + if (projNo && vendorInfo) { + loadDrawings(); + } + }, [projNo, vendorInfo, loadDrawings]); + + // 도면 클릭 핸들러 + const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => { + setSelectedDrawing(drawing); + setDialogOpen(true); + }; + + // 검색 핸들러 + const handleSearch = () => { + loadDrawings(); + }; + + // 새로고침 핸들러 + const handleRefresh = () => { + loadDrawings(); + }; + + // 일괄 업로드 완료 핸들러 + const handleBulkUploadComplete = () => { + loadDrawings(); + }; + + // 필터된 도면 목록 (클라이언트 사이드 필터링) + const filteredDrawings = useMemo(() => { + let result = drawings.filter((drawing) => { + // 도면번호 필터 (공백 포함) + if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) { + return false; + } + + // 도면명 필터 (공백 포함) + if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) { + return false; + } + + // 설계공종 필터 (공백 포함) + if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) { + return false; + } + + // 담당자명 필터 (공백 포함) + if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) && + !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) { + return false; + } + + return true; + }); + + // B4인 경우 Document Type 필터 적용 + if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") { + result = result.filter((drawing) => { + // B4 타입 체크 + if (drawing.DrawingKind !== "B4") return false; + + // B4 도면의 DrawingMoveGbn 체크 + const gttDrawing = drawing as { DrawingMoveGbn?: string }; + + if (documentType === "SHI_INPUT") { + return gttDrawing.DrawingMoveGbn === "도면제출"; + } else if (documentType === "GTT_DELIVERABLES") { + return gttDrawing.DrawingMoveGbn === "도면입수"; + } + return true; + }); + } + + return result; + }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]); + + if (isLoading) { + return ( + + + + + + + + + + + ); + } + + return ( +
+ {/* 에러 메시지 */} + {error && ( + + {error} + + )} + + {/* 안내 메시지 */} + {!projNo && ( + + + + {t("page.selectProject")} + + + )} + + {/* 필터 카드 */} + + + {t("filter.title")} + + +
+ {/* 프로젝트 선택 */} +
+ + +
+ + {/* 도면번호 검색 */} +
+ + setDrawingNo(e.target.value)} + placeholder={t("filter.drawingNoPlaceholder")} + /> +
+ + {/* 도면명 검색 */} +
+ + setDrawingName(e.target.value)} + placeholder={t("filter.drawingNamePlaceholder")} + /> +
+ + {/* 설계공종 검색 */} +
+ + setDiscipline(e.target.value)} + placeholder={t("filter.disciplinePlaceholder")} + /> +
+ + {/* 담당자명 검색 (클라이언트 필터) */} +
+ + setManager(e.target.value)} + placeholder={t("filter.managerPlaceholder")} + /> +
+ + {/* B4(GTT) 전용: Document Type 필터 */} + {vendorInfo?.drawingKind === "B4" && ( +
+ + +
+ )} +
+ +
+ + {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */} + {vendorInfo?.drawingKind === "B4" && ( + + )} + +
+
+
+ + {/* 도면 리스트 테이블 */} + {projNo && vendorInfo && ( + + + + {t("drawingList.title")} + {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`} + + + + + + + )} + + {/* 상세도면 다이얼로그 */} + {vendorInfo && ( + + )} + + {/* B4 일괄 업로드 다이얼로그 (V2) */} + {/* V2: MatchBatchFileDwg+MatchBatchFileDwgEdit API 사용, 별도의 RegisterKind 선택 없이 결과값 기준으로 업로드 */} + {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( + + )} + {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV2를 B4BulkUploadDialog로 변경하세요 */} +
+ ); +} + diff --git a/app/[lng]/partners/(partners)/_dolce-upload/page.tsx b/app/[lng]/partners/(partners)/_dolce-upload/page.tsx new file mode 100644 index 00000000..4d7b1a74 --- /dev/null +++ b/app/[lng]/partners/(partners)/_dolce-upload/page.tsx @@ -0,0 +1,67 @@ +import { Suspense } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import DolceUploadPage from "./dolce-upload-page"; +import { Shell } from "@/components/shell"; + +export const metadata = { + title: "조선 벤더문서 업로드(DOLCE)", + description: "조선 설계문서 업로드 및 관리", +}; + +// ============================================================================ +// 로딩 스켈레톤 +// ============================================================================ + +function DolceUploadSkeleton() { + return ( + + +
+ + +
+
+ + + + +
+ ); +} + +export default async function DolceUploadPageWrapper({ + params, + searchParams, +}: { + params: Promise<{ lng: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const { lng } = await params; + const resolvedParams = await searchParams; + + return ( + + {/* 헤더 */} +
+
+

+ {lng === "ko" + ? "DOLCE 도면 업로드" + : "DOLCE Drawing Upload"} +

+

+ {lng === "ko" + ? "설계문서를 조회하고 업로드할 수 있습니다" + : "View and upload design documents"} +

+
+
+ + {/* 메인 컨텐츠 */} + }> + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/document-list-ship-legacy/page.tsx b/app/[lng]/partners/(partners)/document-list-ship-legacy/page.tsx new file mode 100644 index 00000000..c70a0c03 --- /dev/null +++ b/app/[lng]/partners/(partners)/document-list-ship-legacy/page.tsx @@ -0,0 +1,145 @@ +// page.tsx (간단한 Promise 생성과 로그인 처리) +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsShipDocuCache } from "@/lib/vendor-document-list/validations" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { LogIn } from "lucide-react" +import { getUserVendorDocumentStats, getUserVendorDocuments } from "@/lib/vendor-document-list/enhanced-document-service" +import { UserVendorDocumentDisplay } from "@/components/ship-vendor-document/user-vendor-document-table-container" +import { InformationButton } from "@/components/information/information-button" +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsShipDocuCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // Get session + const session = await getServerSession(authOptions) + + // Check if user is logged in + if (!session || !session.user) { + return ( + +
+
+
+

+ 문서 관리 +

+
+ {/*

+ 소속 회사의 모든 도서/도면을 확인하고 관리합니다. +

*/} +
+
+ +
+
+

로그인이 필요합니다

+

+ 문서를 확인하려면 먼저 로그인하세요. +

+ +
+
+
+ ) + } + + // User is logged in, get user ID + const requesterId = session.user.id ? Number(session.user.id) : null + + if (!requesterId) { + return ( + +
+
+

+ Document Management +

+
+
+
+
+

계정 오류

+

+ 사용자 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. +

+
+
+
+ ) + } + + // 검색 파라미터 정리 + const searchInput = { + ...search, + filters: validFilters, + } + + // Promise 생성 (모든 데이터를 페이지에서 처리) + const documentsPromise = getUserVendorDocuments(requesterId, searchInput) + const statsPromise = getUserVendorDocumentStats(requesterId) + + // Promise.all로 감싸서 전달 + const allPromises = Promise.all([documentsPromise, statsPromise]) + + const statsResult = await documentsPromise + const vendorName = statsResult.vendorInfo?.vendorName || "내 회사" + + + return ( + +
+
+
+

+ {vendorName} Document Management +

+ +
+ +

+ +

+
+
+ + }> + {/* DateRangePicker can go here */} + + + + } + > + + +
+ ) +} + diff --git a/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx b/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx new file mode 100644 index 00000000..29b41136 --- /dev/null +++ b/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx @@ -0,0 +1,718 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useParams } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { InfoIcon, RefreshCw, Search, Upload, Plus, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { useTranslation } from "@/i18n/client"; +import { + UnifiedDwgReceiptItem, + DetailDwgReceiptItem, + FileInfoItem, + fetchDwgReceiptList, + getVendorSessionInfo, + fetchVendorProjects, + fetchDetailDwgReceiptList, + fetchFileInfoList, +} from "@/lib/dolce/actions"; +import { DrawingListTableV2 } from "@/lib/dolce/table/drawing-list-table-v2"; +import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; +import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; +import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns"; +import { createFileListColumns } from "@/lib/dolce/table/file-list-columns"; +// V3: Sync 기능 없이 일괄 업로드 (MatchBatchFileDwg / Edit 사용) +import { B4BulkUploadDialogV3 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v3"; +// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 +// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; +import { AddAndModifyDetailDrawingDialog } from "@/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog"; +import { UploadFilesToDetailDialog } from "@/lib/dolce/dialogs/upload-files-to-detail-dialog"; + +interface DolceUploadPageV2Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Props) { + const params = useParams(); + const lng = params?.lng as string; + const { t } = useTranslation(lng, "dolce"); + + // URL에서 초기 프로젝트 코드 + const initialProjNo = (searchParams.projNo as string) || ""; + + // 상태 관리 + const [drawings, setDrawings] = useState([]); + const [projects, setProjects] = useState>([]); + const [vendorInfo, setVendorInfo] = useState<{ + userId: string; + userName: string; + email: string; + vendorCode: string; + vendorName: string; + drawingKind: "B3" | "B4"; + } | null>(null); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + + // 필터 상태 + const [projNo, setProjNo] = useState(initialProjNo); + const [drawingNo, setDrawingNo] = useState(""); + const [drawingName, setDrawingName] = useState(""); + const [discipline, setDiscipline] = useState(""); + const [manager, setManager] = useState(""); + const [documentType, setDocumentType] = useState("ALL"); // B4 전용 + + // 선택된 도면 및 상세도면 + const [selectedDrawing, setSelectedDrawing] = useState(null); + const [detailDrawings, setDetailDrawings] = useState([]); + const [selectedDetail, setSelectedDetail] = useState(null); + const [files, setFiles] = useState([]); + const [isLoadingDetails, setIsLoadingDetails] = useState(false); + const [isLoadingFiles, setIsLoadingFiles] = useState(false); + + // 다이얼로그 + const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false); + + // 초기 데이터 로드 + const loadInitialData = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + + // 병렬로 데이터 로드 + const [vendorInfoData, projectsData] = await Promise.all([ + getVendorSessionInfo(), + fetchVendorProjects(), + ]); + + setVendorInfo(vendorInfoData as typeof vendorInfo); + setProjects(projectsData); + + // 초기 프로젝트가 있으면 도면 로드 + if (initialProjNo) { + const drawingsData = await fetchDwgReceiptList({ + project: initialProjNo, + drawingKind: vendorInfoData.drawingKind, + drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "", + }); + setDrawings(drawingsData); + } + } catch (err) { + console.error("초기 데이터 로드 실패:", err); + setError(err instanceof Error ? err.message : t("page.initialLoadError")); + toast.error(t("page.initialLoadError")); + } finally { + setIsLoading(false); + } + }, [initialProjNo, t]); + + // 도면 목록 조회 + const loadDrawings = useCallback(async () => { + if (!projNo || !vendorInfo) return; + + try { + setIsRefreshing(true); + setError(null); + + const drawingsData = await fetchDwgReceiptList({ + project: projNo, + drawingKind: vendorInfo.drawingKind, + drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "", + }); + + setDrawings(drawingsData); + toast.success(t("page.drawingLoadSuccess")); + } catch (err) { + console.error("도면 로드 실패:", err); + setError(err instanceof Error ? err.message : t("page.drawingLoadError")); + toast.error(t("page.drawingLoadError")); + } finally { + setIsRefreshing(false); + } + }, [projNo, vendorInfo, t]); + + // 상세도면 목록 로드 + const loadDetailDrawings = useCallback(async () => { + if (!selectedDrawing) { + setDetailDrawings([]); + setSelectedDetail(null); + return; + } + + try { + setIsLoadingDetails(true); + const data = await fetchDetailDwgReceiptList({ + project: selectedDrawing.ProjectNo, + drawingNo: selectedDrawing.DrawingNo, + discipline: selectedDrawing.Discipline, + drawingKind: selectedDrawing.DrawingKind, + userId: "", // 조회 시 모든 사용자의 상세도면을 보기 위해 빈 문자열 전달 + }); + setDetailDrawings(data); + + // 첫 번째 상세도면 자동 선택 + if (data.length > 0) { + setSelectedDetail(data[0]); + } else { + setSelectedDetail(null); + } + } catch (error) { + console.error("상세도면 로드 실패:", error); + toast.error(t("detailDialog.detailLoadError")); + setDetailDrawings([]); + setSelectedDetail(null); + } finally { + setIsLoadingDetails(false); + } + }, [selectedDrawing, t]); + + // 파일 목록 로드 + const loadFiles = useCallback(async () => { + if (!selectedDetail) { + setFiles([]); + return; + } + + try { + setIsLoadingFiles(true); + const data = await fetchFileInfoList(selectedDetail.UploadId); + setFiles(data); + } catch (error) { + console.error("파일 목록 로드 실패:", error); + toast.error(t("detailDialog.fileLoadError")); + setFiles([]); + } finally { + setIsLoadingFiles(false); + } + }, [selectedDetail, t]); + + // 초기 데이터 로드 + useEffect(() => { + loadInitialData(); + }, [loadInitialData]); + + // 프로젝트 변경 시 자동 검색 + useEffect(() => { + if (projNo && vendorInfo) { + loadDrawings(); + } + }, [projNo, vendorInfo, loadDrawings]); + + // 선택된 도면 변경 시 상세도면 로드 + useEffect(() => { + loadDetailDrawings(); + }, [selectedDrawing, loadDetailDrawings]); + + // 선택된 상세도면 변경 시 파일 목록 로드 + useEffect(() => { + loadFiles(); + }, [selectedDetail, loadFiles]); + + // 도면 클릭 핸들러 + const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => { + setSelectedDrawing(drawing); + }; + + // 검색 핸들러 + const handleSearch = () => { + loadDrawings(); + }; + + // 새로고침 핸들러 + const handleRefresh = () => { + loadDrawings(); + }; + + // 상세도면 새로고침 핸들러 + const handleRefreshDetails = () => { + loadDetailDrawings(); + }; + + // 일괄 업로드 완료 핸들러 + const handleBulkUploadComplete = () => { + loadDrawings(); + }; + + // 상세도면 추가 완료 핸들러 + const handleAddComplete = () => { + setAddDialogOpen(false); + loadDetailDrawings(); + }; + + // 파일 업로드 완료 핸들러 + const handleUploadComplete = () => { + setUploadFilesDialogOpen(false); + loadFiles(); + }; + + // 파일 다운로드 핸들러 + const handleDownload = async (file: FileInfoItem) => { + try { + toast.info(t("detailDialog.downloadPreparing")); + + // 파일 생성자의 userId를 사용하여 다운로드 + const response = await fetch("/api/dolce/download", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fileId: file.FileId, + userId: file.CreateUserId, // 파일 생성자의 ID 사용 + fileName: file.FileName, + }), + }); + + if (!response.ok) { + throw new Error(t("detailDialog.downloadError")); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = file.FileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + toast.success(t("detailDialog.downloadSuccess")); + } catch (error) { + console.error("파일 다운로드 실패:", error); + toast.error(t("detailDialog.downloadError")); + } + }; + + // 필터된 도면 목록 (클라이언트 사이드 필터링) + const filteredDrawings = useMemo(() => { + let result = drawings.filter((drawing) => { + // 도면번호 필터 (공백 포함) + if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) { + return false; + } + + // 도면명 필터 (공백 포함) + if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) { + return false; + } + + // 설계공종 필터 (공백 포함) + if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) { + return false; + } + + // 담당자명 필터 (공백 포함) + if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) && + !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) { + return false; + } + + return true; + }); + + // B4인 경우 Document Type 필터 적용 + if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") { + result = result.filter((drawing) => { + // B4 타입 체크 + if (drawing.DrawingKind !== "B4") return false; + + // B4 도면의 DrawingMoveGbn 체크 + const gttDrawing = drawing as { DrawingMoveGbn?: string }; + + if (documentType === "SHI_INPUT") { + return gttDrawing.DrawingMoveGbn === "도면제출"; + } else if (documentType === "GTT_DELIVERABLES") { + return gttDrawing.DrawingMoveGbn === "도면입수"; + } + return true; + }); + } + + return result; + }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]); + + // RegisterId + UploadId 조합으로 고유 ID 생성 + const getDetailDrawingId = (detail: DetailDwgReceiptItem) => { + return `${detail.RegisterId}_${detail.UploadId}`; + }; + + // 도면 고유 ID 생성 + const getDrawingId = (drawing: UnifiedDwgReceiptItem) => { + return `${drawing.ProjectNo}_${drawing.DrawingNo}_${drawing.Discipline}`; + }; + + // B4인 경우 "도면입수"인 건만 상세도면 추가 및 파일 첨부 가능 + // B3인 경우 모든 건에 대해 가능 + const canAddDetailDrawing = vendorInfo && ( + vendorInfo.drawingKind === "B3" || + (vendorInfo.drawingKind === "B4" && selectedDrawing && 'DrawingMoveGbn' in selectedDrawing && selectedDrawing.DrawingMoveGbn === "도면입수") + ); + + const fileColumns = createFileListColumns({ onDownload: handleDownload, lng }); + + if (isLoading) { + return ( + + + + + + + + + + + ); + } + + return ( +
+ {/* 에러 메시지 */} + {error && ( + + {error} + + )} + + {/* 안내 메시지 */} + {!projNo && ( + + + + {t("page.selectProject")} + + + )} + + {/* 필터 카드 - 슬림하게 */} + + + {t("filter.title")} + + +
+ {/* 프로젝트 선택 */} +
+ + +
+ + {/* 도면번호 검색 */} +
+ + setDrawingNo(e.target.value)} + placeholder={t("filter.drawingNoPlaceholder")} + /> +
+ + {/* 도면명 검색 */} +
+ + setDrawingName(e.target.value)} + placeholder={t("filter.drawingNamePlaceholder")} + /> +
+ + {/* 설계공종 검색 */} +
+ + setDiscipline(e.target.value)} + placeholder={t("filter.disciplinePlaceholder")} + /> +
+ + {/* 담당자명 검색 */} +
+ + setManager(e.target.value)} + placeholder={t("filter.managerPlaceholder")} + /> +
+ + {/* B4(GTT) 전용: Document Type 필터 */} + {vendorInfo?.drawingKind === "B4" && ( +
+ + +
+ )} +
+ +
+ + {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */} + {vendorInfo?.drawingKind === "B4" && ( + + )} + +
+
+
+ + {/* 도면 리스트 테이블 - 항상 렌더링 */} + + + + {t("drawingList.title")} + {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`} + + + + {!projNo || !vendorInfo ? ( +
+
+ +

{t("page.selectProject")}

+
+
+ ) : isRefreshing ? ( +
+ +
+ ) : ( + + )} +
+
+ + {/* 하단: 상세도면리스트 + 파일리스트 - 항상 렌더링 */} +
+ {/* 좌측: 상세도면 리스트 */} + + + + {t("detailDialog.detailListTitle")} + {selectedDrawing && ( + + {selectedDrawing.DrawingNo} + + )} + +
+ + {canAddDetailDrawing && ( + + )} +
+
+ + {!selectedDrawing ? ( +
+
+ +

도면을 선택해주세요

+
+
+ ) : isLoadingDetails ? ( +
+ +
+ ) : ( + + columns={createDetailDrawingColumns(lng, t)} + data={detailDrawings} + onRowClick={setSelectedDetail} + selectedRow={selectedDetail || undefined} + getRowId={getDetailDrawingId} + minHeight="400px" + defaultPageSize={10} + /> + )} +
+
+ + {/* 우측: 첨부파일 리스트 */} + + + + {t("detailDialog.fileListTitle")} + {selectedDetail && ( + + Rev. {selectedDetail.DrawingRevNo} + + )} + + {selectedDetail && canAddDetailDrawing && ( + + )} + + + {!selectedDetail ? ( +
+
+ +

{t("detailDialog.selectDetailDrawing")}

+
+
+ ) : isLoadingFiles ? ( +
+ +
+ ) : ( + + )} +
+
+
+ + {/* B4 일괄 업로드 다이얼로그 (V3) */} + {/* V3: Sync 기능 없이 일괄 업로드 (MatchBatchFileDwg / Edit 사용) */} + {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( + + )} + {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV3를 B4BulkUploadDialog로 변경하세요 */} + + {/* 상세도면 추가 다이얼로그 */} + {vendorInfo && selectedDrawing && ( + + )} + + {/* 파일 업로드 다이얼로그 */} + {vendorInfo && selectedDetail && ( + + )} +
+ ); +} + diff --git a/app/[lng]/partners/(partners)/document-list-ship/page.tsx b/app/[lng]/partners/(partners)/document-list-ship/page.tsx index c70a0c03..9ce7c6c6 100644 --- a/app/[lng]/partners/(partners)/document-list-ship/page.tsx +++ b/app/[lng]/partners/(partners)/document-list-ship/page.tsx @@ -1,145 +1,69 @@ -// page.tsx (간단한 Promise 생성과 로그인 처리) -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsShipDocuCache } from "@/lib/vendor-document-list/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getUserVendorDocumentStats, getUserVendorDocuments } from "@/lib/vendor-document-list/enhanced-document-service" -import { UserVendorDocumentDisplay } from "@/components/ship-vendor-document/user-vendor-document-table-container" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsShipDocuCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - return ( - -
-
-
-

- 문서 관리 -

-
- {/*

- 소속 회사의 모든 도서/도면을 확인하고 관리합니다. -

*/} -
-
- -
-
-

로그인이 필요합니다

-

- 문서를 확인하려면 먼저 로그인하세요. -

- -
-
-
- ) - } - - // User is logged in, get user ID - const requesterId = session.user.id ? Number(session.user.id) : null +import { Suspense } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import DolceUploadPageV2 from "./dolce-upload-page-v2"; +import { Shell } from "@/components/shell"; - if (!requesterId) { - return ( - -
-
-

- Document Management -

-
-
-
-
-

계정 오류

-

- 사용자 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. -

-
-
-
- ) - } +export const metadata = { + title: "조선 벤더문서 업로드(DOLCE) V2", + description: "조선 설계문서 업로드 및 관리 - 분할 레이아웃", +}; - // 검색 파라미터 정리 - const searchInput = { - ...search, - filters: validFilters, - } +// ============================================================================ +// 로딩 스켈레톤 +// ============================================================================ - // Promise 생성 (모든 데이터를 페이지에서 처리) - const documentsPromise = getUserVendorDocuments(requesterId, searchInput) - const statsPromise = getUserVendorDocumentStats(requesterId) - - // Promise.all로 감싸서 전달 - const allPromises = Promise.all([documentsPromise, statsPromise]) - - const statsResult = await documentsPromise - const vendorName = statsResult.vendorInfo?.vendorName || "내 회사" - - - return ( - -
-
-
-

- {vendorName} Document Management -

- -
- -

- -

-
-
+function DolceUploadSkeleton() { + return ( +
+ + + + + + + + + + + + + + + + +
+ ); +} - }> - {/* DateRangePicker can go here */} - +export default async function DolceUploadPageWrapper({ + params, + searchParams, +}: { + params: Promise<{ lng: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const { lng } = await params; + const resolvedParams = await searchParams; + + return ( + + {/* 헤더 */} +
+
+

+ {lng === "ko" + ? "조선 도면 업로드" + : "Shipbuilding Drawing Upload"} +

+
+
- - } - > - - -
- ) + {/* 메인 컨텐츠 */} + }> + + +
+ ); } diff --git a/app/[lng]/partners/(partners)/document-upload/page.tsx b/app/[lng]/partners/(partners)/document-upload/page.tsx deleted file mode 100644 index c67a603c..00000000 --- a/app/[lng]/partners/(partners)/document-upload/page.tsx +++ /dev/null @@ -1,154 +0,0 @@ -// app/(vendor)/stage-submissions/page.tsx -import * as React from "react" -import { searchParamsCache } from "@/lib/vendor-document-list/plant/upload/validation" -import { StageSubmissionsTable } from "@/lib/vendor-document-list/plant/upload/table" -import { redirect } from "next/navigation" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { getServerSession } from 'next-auth/next' -import { authOptions } from '@/app/api/auth/[...nextauth]/route' -import { getStageSubmissions, getProjects, getSubmissionStats } from "@/lib/vendor-document-list/plant/upload/service" -import db from "@/db/db" -import { eq } from "drizzle-orm" -import { vendors } from "@/db/schema" - -export default async function StageSubmissionsPage({ - searchParams, -}: { - searchParams: Promise> -}) { - // Session 체크 - const session = await getServerSession(authOptions) - - if (!session?.user?.companyId) { - redirect("/partners") - } - - - const vendor = await db.query.vendors.findFirst({ - where: eq(vendors.id, session.user.companyId), - columns: { - vendorName: true, - vendorCode: true, - } - }) - - const params = searchParamsCache.parse(await searchParams) - - const submissionsPromise = getStageSubmissions(params) - const projectsPromise = getProjects() - const statsPromise = getSubmissionStats() - - const [submissions, projects, stats] = await Promise.all([ - submissionsPromise, - projectsPromise, - statsPromise - ]) - - return ( -
- {/* Header */} -
-
-

My Stage Submissions

-

- Manage document submissions for your approved stages (SWP) -

-
-
- {/* */} - {/* Company: */} - {vendor.vendorName || "Your Company"} - -

- Buyer Approved Documents -

- {/*
*/} -
-
- - {/* Stats Cards */} -
- - - Pending Submissions - - -
- {stats.pending} -
-
-
- - - - Overdue - - -
- {stats.overdue} -
-
-
- - - - Awaiting Sync - - -
- {stats.awaitingSync} -
-
-
- - - - Completed - - -
- {stats.completed} -
-
-
-
- - {/* Main Table */} - - - Your Submission List - - View and manage document submissions for your company's stages - - - - -
- Loading your submissions... -
-
- } - > - ({ id: p.id, code: p.code || "" })) } - ])} - selectedProjectId={params.projectId} - - /> - - - - - ) -} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx deleted file mode 100644 index 29b41136..00000000 --- a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx +++ /dev/null @@ -1,718 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useMemo } from "react"; -import { useParams } from "next/navigation"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { InfoIcon, RefreshCw, Search, Upload, Plus, Loader2 } from "lucide-react"; -import { toast } from "sonner"; -import { useTranslation } from "@/i18n/client"; -import { - UnifiedDwgReceiptItem, - DetailDwgReceiptItem, - FileInfoItem, - fetchDwgReceiptList, - getVendorSessionInfo, - fetchVendorProjects, - fetchDetailDwgReceiptList, - fetchFileInfoList, -} from "@/lib/dolce/actions"; -import { DrawingListTableV2 } from "@/lib/dolce/table/drawing-list-table-v2"; -import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; -import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; -import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns"; -import { createFileListColumns } from "@/lib/dolce/table/file-list-columns"; -// V3: Sync 기능 없이 일괄 업로드 (MatchBatchFileDwg / Edit 사용) -import { B4BulkUploadDialogV3 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v3"; -// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 -// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; -import { AddAndModifyDetailDrawingDialog } from "@/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog"; -import { UploadFilesToDetailDialog } from "@/lib/dolce/dialogs/upload-files-to-detail-dialog"; - -interface DolceUploadPageV2Props { - searchParams: { [key: string]: string | string[] | undefined }; -} - -export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Props) { - const params = useParams(); - const lng = params?.lng as string; - const { t } = useTranslation(lng, "dolce"); - - // URL에서 초기 프로젝트 코드 - const initialProjNo = (searchParams.projNo as string) || ""; - - // 상태 관리 - const [drawings, setDrawings] = useState([]); - const [projects, setProjects] = useState>([]); - const [vendorInfo, setVendorInfo] = useState<{ - userId: string; - userName: string; - email: string; - vendorCode: string; - vendorName: string; - drawingKind: "B3" | "B4"; - } | null>(null); - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [error, setError] = useState(null); - - // 필터 상태 - const [projNo, setProjNo] = useState(initialProjNo); - const [drawingNo, setDrawingNo] = useState(""); - const [drawingName, setDrawingName] = useState(""); - const [discipline, setDiscipline] = useState(""); - const [manager, setManager] = useState(""); - const [documentType, setDocumentType] = useState("ALL"); // B4 전용 - - // 선택된 도면 및 상세도면 - const [selectedDrawing, setSelectedDrawing] = useState(null); - const [detailDrawings, setDetailDrawings] = useState([]); - const [selectedDetail, setSelectedDetail] = useState(null); - const [files, setFiles] = useState([]); - const [isLoadingDetails, setIsLoadingDetails] = useState(false); - const [isLoadingFiles, setIsLoadingFiles] = useState(false); - - // 다이얼로그 - const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); - const [addDialogOpen, setAddDialogOpen] = useState(false); - const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false); - - // 초기 데이터 로드 - const loadInitialData = useCallback(async () => { - try { - setIsLoading(true); - setError(null); - - // 병렬로 데이터 로드 - const [vendorInfoData, projectsData] = await Promise.all([ - getVendorSessionInfo(), - fetchVendorProjects(), - ]); - - setVendorInfo(vendorInfoData as typeof vendorInfo); - setProjects(projectsData); - - // 초기 프로젝트가 있으면 도면 로드 - if (initialProjNo) { - const drawingsData = await fetchDwgReceiptList({ - project: initialProjNo, - drawingKind: vendorInfoData.drawingKind, - drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "", - }); - setDrawings(drawingsData); - } - } catch (err) { - console.error("초기 데이터 로드 실패:", err); - setError(err instanceof Error ? err.message : t("page.initialLoadError")); - toast.error(t("page.initialLoadError")); - } finally { - setIsLoading(false); - } - }, [initialProjNo, t]); - - // 도면 목록 조회 - const loadDrawings = useCallback(async () => { - if (!projNo || !vendorInfo) return; - - try { - setIsRefreshing(true); - setError(null); - - const drawingsData = await fetchDwgReceiptList({ - project: projNo, - drawingKind: vendorInfo.drawingKind, - drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "", - }); - - setDrawings(drawingsData); - toast.success(t("page.drawingLoadSuccess")); - } catch (err) { - console.error("도면 로드 실패:", err); - setError(err instanceof Error ? err.message : t("page.drawingLoadError")); - toast.error(t("page.drawingLoadError")); - } finally { - setIsRefreshing(false); - } - }, [projNo, vendorInfo, t]); - - // 상세도면 목록 로드 - const loadDetailDrawings = useCallback(async () => { - if (!selectedDrawing) { - setDetailDrawings([]); - setSelectedDetail(null); - return; - } - - try { - setIsLoadingDetails(true); - const data = await fetchDetailDwgReceiptList({ - project: selectedDrawing.ProjectNo, - drawingNo: selectedDrawing.DrawingNo, - discipline: selectedDrawing.Discipline, - drawingKind: selectedDrawing.DrawingKind, - userId: "", // 조회 시 모든 사용자의 상세도면을 보기 위해 빈 문자열 전달 - }); - setDetailDrawings(data); - - // 첫 번째 상세도면 자동 선택 - if (data.length > 0) { - setSelectedDetail(data[0]); - } else { - setSelectedDetail(null); - } - } catch (error) { - console.error("상세도면 로드 실패:", error); - toast.error(t("detailDialog.detailLoadError")); - setDetailDrawings([]); - setSelectedDetail(null); - } finally { - setIsLoadingDetails(false); - } - }, [selectedDrawing, t]); - - // 파일 목록 로드 - const loadFiles = useCallback(async () => { - if (!selectedDetail) { - setFiles([]); - return; - } - - try { - setIsLoadingFiles(true); - const data = await fetchFileInfoList(selectedDetail.UploadId); - setFiles(data); - } catch (error) { - console.error("파일 목록 로드 실패:", error); - toast.error(t("detailDialog.fileLoadError")); - setFiles([]); - } finally { - setIsLoadingFiles(false); - } - }, [selectedDetail, t]); - - // 초기 데이터 로드 - useEffect(() => { - loadInitialData(); - }, [loadInitialData]); - - // 프로젝트 변경 시 자동 검색 - useEffect(() => { - if (projNo && vendorInfo) { - loadDrawings(); - } - }, [projNo, vendorInfo, loadDrawings]); - - // 선택된 도면 변경 시 상세도면 로드 - useEffect(() => { - loadDetailDrawings(); - }, [selectedDrawing, loadDetailDrawings]); - - // 선택된 상세도면 변경 시 파일 목록 로드 - useEffect(() => { - loadFiles(); - }, [selectedDetail, loadFiles]); - - // 도면 클릭 핸들러 - const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => { - setSelectedDrawing(drawing); - }; - - // 검색 핸들러 - const handleSearch = () => { - loadDrawings(); - }; - - // 새로고침 핸들러 - const handleRefresh = () => { - loadDrawings(); - }; - - // 상세도면 새로고침 핸들러 - const handleRefreshDetails = () => { - loadDetailDrawings(); - }; - - // 일괄 업로드 완료 핸들러 - const handleBulkUploadComplete = () => { - loadDrawings(); - }; - - // 상세도면 추가 완료 핸들러 - const handleAddComplete = () => { - setAddDialogOpen(false); - loadDetailDrawings(); - }; - - // 파일 업로드 완료 핸들러 - const handleUploadComplete = () => { - setUploadFilesDialogOpen(false); - loadFiles(); - }; - - // 파일 다운로드 핸들러 - const handleDownload = async (file: FileInfoItem) => { - try { - toast.info(t("detailDialog.downloadPreparing")); - - // 파일 생성자의 userId를 사용하여 다운로드 - const response = await fetch("/api/dolce/download", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - fileId: file.FileId, - userId: file.CreateUserId, // 파일 생성자의 ID 사용 - fileName: file.FileName, - }), - }); - - if (!response.ok) { - throw new Error(t("detailDialog.downloadError")); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = file.FileName; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - - toast.success(t("detailDialog.downloadSuccess")); - } catch (error) { - console.error("파일 다운로드 실패:", error); - toast.error(t("detailDialog.downloadError")); - } - }; - - // 필터된 도면 목록 (클라이언트 사이드 필터링) - const filteredDrawings = useMemo(() => { - let result = drawings.filter((drawing) => { - // 도면번호 필터 (공백 포함) - if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) { - return false; - } - - // 도면명 필터 (공백 포함) - if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) { - return false; - } - - // 설계공종 필터 (공백 포함) - if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) { - return false; - } - - // 담당자명 필터 (공백 포함) - if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) && - !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) { - return false; - } - - return true; - }); - - // B4인 경우 Document Type 필터 적용 - if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") { - result = result.filter((drawing) => { - // B4 타입 체크 - if (drawing.DrawingKind !== "B4") return false; - - // B4 도면의 DrawingMoveGbn 체크 - const gttDrawing = drawing as { DrawingMoveGbn?: string }; - - if (documentType === "SHI_INPUT") { - return gttDrawing.DrawingMoveGbn === "도면제출"; - } else if (documentType === "GTT_DELIVERABLES") { - return gttDrawing.DrawingMoveGbn === "도면입수"; - } - return true; - }); - } - - return result; - }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]); - - // RegisterId + UploadId 조합으로 고유 ID 생성 - const getDetailDrawingId = (detail: DetailDwgReceiptItem) => { - return `${detail.RegisterId}_${detail.UploadId}`; - }; - - // 도면 고유 ID 생성 - const getDrawingId = (drawing: UnifiedDwgReceiptItem) => { - return `${drawing.ProjectNo}_${drawing.DrawingNo}_${drawing.Discipline}`; - }; - - // B4인 경우 "도면입수"인 건만 상세도면 추가 및 파일 첨부 가능 - // B3인 경우 모든 건에 대해 가능 - const canAddDetailDrawing = vendorInfo && ( - vendorInfo.drawingKind === "B3" || - (vendorInfo.drawingKind === "B4" && selectedDrawing && 'DrawingMoveGbn' in selectedDrawing && selectedDrawing.DrawingMoveGbn === "도면입수") - ); - - const fileColumns = createFileListColumns({ onDownload: handleDownload, lng }); - - if (isLoading) { - return ( - - - - - - - - - - - ); - } - - return ( -
- {/* 에러 메시지 */} - {error && ( - - {error} - - )} - - {/* 안내 메시지 */} - {!projNo && ( - - - - {t("page.selectProject")} - - - )} - - {/* 필터 카드 - 슬림하게 */} - - - {t("filter.title")} - - -
- {/* 프로젝트 선택 */} -
- - -
- - {/* 도면번호 검색 */} -
- - setDrawingNo(e.target.value)} - placeholder={t("filter.drawingNoPlaceholder")} - /> -
- - {/* 도면명 검색 */} -
- - setDrawingName(e.target.value)} - placeholder={t("filter.drawingNamePlaceholder")} - /> -
- - {/* 설계공종 검색 */} -
- - setDiscipline(e.target.value)} - placeholder={t("filter.disciplinePlaceholder")} - /> -
- - {/* 담당자명 검색 */} -
- - setManager(e.target.value)} - placeholder={t("filter.managerPlaceholder")} - /> -
- - {/* B4(GTT) 전용: Document Type 필터 */} - {vendorInfo?.drawingKind === "B4" && ( -
- - -
- )} -
- -
- - {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */} - {vendorInfo?.drawingKind === "B4" && ( - - )} - -
-
-
- - {/* 도면 리스트 테이블 - 항상 렌더링 */} - - - - {t("drawingList.title")} - {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`} - - - - {!projNo || !vendorInfo ? ( -
-
- -

{t("page.selectProject")}

-
-
- ) : isRefreshing ? ( -
- -
- ) : ( - - )} -
-
- - {/* 하단: 상세도면리스트 + 파일리스트 - 항상 렌더링 */} -
- {/* 좌측: 상세도면 리스트 */} - - - - {t("detailDialog.detailListTitle")} - {selectedDrawing && ( - - {selectedDrawing.DrawingNo} - - )} - -
- - {canAddDetailDrawing && ( - - )} -
-
- - {!selectedDrawing ? ( -
-
- -

도면을 선택해주세요

-
-
- ) : isLoadingDetails ? ( -
- -
- ) : ( - - columns={createDetailDrawingColumns(lng, t)} - data={detailDrawings} - onRowClick={setSelectedDetail} - selectedRow={selectedDetail || undefined} - getRowId={getDetailDrawingId} - minHeight="400px" - defaultPageSize={10} - /> - )} -
-
- - {/* 우측: 첨부파일 리스트 */} - - - - {t("detailDialog.fileListTitle")} - {selectedDetail && ( - - Rev. {selectedDetail.DrawingRevNo} - - )} - - {selectedDetail && canAddDetailDrawing && ( - - )} - - - {!selectedDetail ? ( -
-
- -

{t("detailDialog.selectDetailDrawing")}

-
-
- ) : isLoadingFiles ? ( -
- -
- ) : ( - - )} -
-
-
- - {/* B4 일괄 업로드 다이얼로그 (V3) */} - {/* V3: Sync 기능 없이 일괄 업로드 (MatchBatchFileDwg / Edit 사용) */} - {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( - - )} - {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV3를 B4BulkUploadDialog로 변경하세요 */} - - {/* 상세도면 추가 다이얼로그 */} - {vendorInfo && selectedDrawing && ( - - )} - - {/* 파일 업로드 다이얼로그 */} - {vendorInfo && selectedDetail && ( - - )} -
- ); -} - diff --git a/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx b/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx deleted file mode 100644 index 9ce7c6c6..00000000 --- a/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Suspense } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import DolceUploadPageV2 from "./dolce-upload-page-v2"; -import { Shell } from "@/components/shell"; - -export const metadata = { - title: "조선 벤더문서 업로드(DOLCE) V2", - description: "조선 설계문서 업로드 및 관리 - 분할 레이아웃", -}; - -// ============================================================================ -// 로딩 스켈레톤 -// ============================================================================ - -function DolceUploadSkeleton() { - return ( -
- - - - - - - - - - - - - - - - -
- ); -} - -export default async function DolceUploadPageWrapper({ - params, - searchParams, -}: { - params: Promise<{ lng: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - const { lng } = await params; - const resolvedParams = await searchParams; - - return ( - - {/* 헤더 */} -
-
-

- {lng === "ko" - ? "조선 도면 업로드" - : "Shipbuilding Drawing Upload"} -

-
-
- - {/* 메인 컨텐츠 */} - }> - - -
- ); -} - diff --git a/app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx b/app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx deleted file mode 100644 index 55bedb38..00000000 --- a/app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx +++ /dev/null @@ -1,590 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -"use client"; - -import { useState, useEffect, useCallback, useMemo } from "react"; -import { useParams } from "next/navigation"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { InfoIcon, RefreshCw, Search, Upload, Plus, Cloud } from "lucide-react"; -import { toast } from "sonner"; -import { useTranslation } from "@/i18n/client"; -import { - UnifiedDwgReceiptItem, - DetailDwgReceiptItem, - FileInfoItem, - fetchDwgReceiptList, - getVendorSessionInfo, - fetchVendorProjects, - fetchDetailDwgReceiptListV2, - fetchFileInfoListV2, - fetchPendingSyncItems, - deleteLocalFile, -} from "@/lib/dolce-v2/actions"; -import { DrawingListTableV2 } from "@/lib/dolce/table/drawing-list-table-v2"; -import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; -import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; -import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns"; -import { createFileListColumns } from "@/lib/dolce/table/file-list-columns"; - -// 다이얼로그 (V2/V3) -import { B4BulkUploadDialogV3Sync } from "@/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3"; -import { AddAndModifyDetailDrawingDialogV2 } from "@/lib/dolce-v2/dialogs/add-and-modify-detail-drawing-dialog-v2"; -import { UploadFilesToDetailDialogV2 } from "@/lib/dolce-v2/dialogs/upload-files-to-detail-dialog-v2"; -import { SyncItemsDialog } from "@/lib/dolce-v2/dialogs/sync-items-dialog"; - -interface DolceUploadPageV3Props { - searchParams: { [key: string]: string | string[] | undefined }; -} - -export default function DolceUploadPageV3({ searchParams }: DolceUploadPageV3Props) { - const params = useParams(); - const lng = params?.lng as string; - const { t } = useTranslation(lng, "dolce"); - - // URL에서 초기 프로젝트 코드 - const initialProjNo = (searchParams.projNo as string) || ""; - - // 상태 관리 - const [drawings, setDrawings] = useState([]); - const [projects, setProjects] = useState>([]); - const [vendorInfo, setVendorInfo] = useState<{ - userId: string; - userName: string; - email: string; - vendorCode: string; - vendorName: string; - drawingKind: "B3" | "B4"; - } | null>(null); - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [error, setError] = useState(null); - - // 필터 상태 - const [projNo, setProjNo] = useState(initialProjNo); - const [drawingNo, setDrawingNo] = useState(""); - const [drawingName, setDrawingName] = useState(""); - const [discipline, setDiscipline] = useState(""); - const [manager, setManager] = useState(""); - const [documentType, setDocumentType] = useState("ALL"); // B4 전용 - - // 선택된 도면 및 상세도면 - const [selectedDrawing, setSelectedDrawing] = useState(null); - const [detailDrawings, setDetailDrawings] = useState([]); - const [selectedDetail, setSelectedDetail] = useState(null); - const [files, setFiles] = useState([]); - const [isLoadingDetails, setIsLoadingDetails] = useState(false); - const [, setIsLoadingFiles] = useState(false); - const [downloadingFileId, setDownloadingFileId] = useState(null); - - // 다이얼로그 상태 - const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); - const [addDialogOpen, setAddDialogOpen] = useState(false); - const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false); - const [syncDialogOpen, setSyncDialogOpen] = useState(false); - - // 동기화 상태 - const [pendingSyncCount, setPendingSyncCount] = useState(0); - - // 미동기화 건수 확인 - const checkPendingSync = useCallback(async () => { - if (!projNo || !vendorInfo) return; - try { - const items = await fetchPendingSyncItems({ projectNo: projNo, userId: vendorInfo.userId }); - setPendingSyncCount(items.length); - } catch (e) { - console.error("Failed to check pending sync items", e); - } - }, [projNo, vendorInfo]); - - // 초기 데이터 로드 - const loadInitialData = useCallback(async () => { - try { - setIsLoading(true); - setError(null); - - const [vendorInfoData, projectsData] = await Promise.all([ - getVendorSessionInfo(), - fetchVendorProjects(), - ]); - - setVendorInfo(vendorInfoData as typeof vendorInfo); - setProjects(projectsData); - - if (initialProjNo) { - const drawingsData = await fetchDwgReceiptList({ - project: initialProjNo, - drawingKind: vendorInfoData.drawingKind, - drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "", - }); - setDrawings(drawingsData); - } - } catch (err) { - console.error("초기 데이터 로드 실패:", err); - setError(err instanceof Error ? err.message : t("page.initialLoadError")); - toast.error(t("page.initialLoadError")); - } finally { - setIsLoading(false); - } - }, [initialProjNo, t]); - - // 도면 목록 조회 - const loadDrawings = useCallback(async () => { - if (!projNo || !vendorInfo) return; - - try { - setIsRefreshing(true); - setError(null); - - const drawingsData = await fetchDwgReceiptList({ - project: projNo, - drawingKind: vendorInfo.drawingKind, - drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "", - }); - - setDrawings(drawingsData); - toast.success(t("page.drawingLoadSuccess")); - - // 동기화 상태 체크 - checkPendingSync(); - - } catch (err) { - console.error("도면 로드 실패:", err); - setError(err instanceof Error ? err.message : t("page.drawingLoadError")); - toast.error(t("page.drawingLoadError")); - } finally { - setIsRefreshing(false); - } - }, [projNo, vendorInfo, t, checkPendingSync]); - - // 상세도면 목록 로드 (V2 API 사용) - const loadDetailDrawings = useCallback(async () => { - if (!selectedDrawing) { - setDetailDrawings([]); - setSelectedDetail(null); - return; - } - - try { - setIsLoadingDetails(true); - // V2: 로컬 임시 저장 건 포함 조회 - const data = await fetchDetailDwgReceiptListV2({ - project: selectedDrawing.ProjectNo, - drawingNo: selectedDrawing.DrawingNo, - discipline: selectedDrawing.Discipline, - drawingKind: selectedDrawing.DrawingKind, - userId: vendorInfo?.userId || "", - }); - setDetailDrawings(data); - - if (data.length > 0) { - setSelectedDetail(data[0]); - } else { - setSelectedDetail(null); - } - - // 동기화 상태 체크 - checkPendingSync(); - - } catch (error) { - console.error("상세도면 로드 실패:", error); - toast.error(t("detailDialog.detailLoadError")); - setDetailDrawings([]); - setSelectedDetail(null); - } finally { - setIsLoadingDetails(false); - } - }, [selectedDrawing, vendorInfo, t, checkPendingSync]); - - // 파일 목록 로드 (V2 API 사용) - const loadFiles = useCallback(async () => { - if (!selectedDetail) { - setFiles([]); - return; - } - - try { - setIsLoadingFiles(true); - // V2: 로컬 임시 파일 포함 조회 - const data = await fetchFileInfoListV2(selectedDetail.UploadId); - setFiles(data); - - // 동기화 상태 체크 - checkPendingSync(); - - } catch (error) { - console.error("파일 목록 로드 실패:", error); - toast.error(t("detailDialog.fileLoadError")); - setFiles([]); - } finally { - setIsLoadingFiles(false); - } - }, [selectedDetail, t, checkPendingSync]); - - // 동기화 완료 핸들러 - const handleSyncComplete = useCallback(() => { - checkPendingSync(); - if (selectedDrawing) loadDetailDrawings(); - if (selectedDetail) loadFiles(); - }, [checkPendingSync, selectedDrawing, selectedDetail, loadDetailDrawings, loadFiles]); - - // 초기 데이터 로드 - useEffect(() => { - loadInitialData(); - }, [loadInitialData]); - - // 프로젝트 변경 시 자동 검색 - useEffect(() => { - if (projNo && vendorInfo) { - loadDrawings(); - } - }, [projNo, vendorInfo, loadDrawings]); - - // 선택된 도면 변경 시 상세도면 로드 - useEffect(() => { - loadDetailDrawings(); - }, [selectedDrawing, loadDetailDrawings]); - - // 선택된 상세도면 변경 시 파일 목록 로드 - useEffect(() => { - loadFiles(); - }, [selectedDetail, loadFiles]); - - const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => { - setSelectedDrawing(drawing); - }; - - const handleSearch = () => { - loadDrawings(); - }; - - const handleRefresh = () => { - loadDrawings(); - }; - - const handleRefreshDetails = () => { - loadDetailDrawings(); - }; - - // 완료 핸들러들 - const handleBulkUploadComplete = () => { - loadDrawings(); - checkPendingSync(); - }; - const handleAddComplete = () => { - setAddDialogOpen(false); - loadDetailDrawings(); - checkPendingSync(); - }; - const handleUploadComplete = () => { - setUploadFilesDialogOpen(false); - loadFiles(); - checkPendingSync(); - }; - - const handleDeleteFile = async (file: FileInfoItem) => { - if (!confirm(lng === "ko" ? "정말로 파일을 삭제하시겠습니까?" : "Are you sure you want to delete this file?")) return; - - try { - const result = await deleteLocalFile(file.FileId); - if (result.success) { - toast.success(lng === "ko" ? "파일이 삭제되었습니다." : "File deleted."); - loadFiles(); - checkPendingSync(); - } else { - throw new Error(result.error); - } - } catch (e) { - console.error("File delete failed", e); - toast.error(lng === "ko" ? "파일 삭제 실패" : "File delete failed"); - } - }; - - const handleDownload = async (file: FileInfoItem) => { - try { - setDownloadingFileId(file.FileId); - toast.info(t("detailDialog.downloadPreparing")); - const response = await fetch("/api/dolce/download", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - fileId: file.FileId, - userId: file.CreateUserId, - fileName: file.FileName, - }), - }); - - if (!response.ok) throw new Error(t("detailDialog.downloadError")); - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = file.FileName; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - toast.success(t("detailDialog.downloadSuccess")); - } catch (error) { - console.error("파일 다운로드 실패:", error); - toast.error(t("detailDialog.downloadError")); - } finally { - setDownloadingFileId(null); - } - }; - - // 필터 로직 - const filteredDrawings = useMemo(() => { - let result = drawings.filter((drawing) => { - if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) return false; - if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) return false; - if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) return false; - if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) && - !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) return false; - return true; - }); - - if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") { - result = result.filter((drawing) => { - if (drawing.DrawingKind !== "B4") return false; - const gttDrawing = drawing as { DrawingMoveGbn?: string }; - if (documentType === "SHI_INPUT") return gttDrawing.DrawingMoveGbn === "도면제출"; - else if (documentType === "GTT_DELIVERABLES") return gttDrawing.DrawingMoveGbn === "도면입수"; - return true; - }); - } - return result; - }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]); - - const getDetailDrawingId = (detail: DetailDwgReceiptItem) => `${detail.RegisterId}_${detail.UploadId}`; - const getDrawingId = (drawing: UnifiedDwgReceiptItem) => `${drawing.ProjectNo}_${drawing.DrawingNo}_${drawing.Discipline}`; - - const canAddDetailDrawing = vendorInfo && ( - vendorInfo.drawingKind === "B3" || - (vendorInfo.drawingKind === "B4" && selectedDrawing && 'DrawingMoveGbn' in selectedDrawing && selectedDrawing.DrawingMoveGbn === "도면입수") - ); - - // 파일 리스트 컬럼 정의 - const fileColumns = createFileListColumns({ - onDownload: handleDownload, - onDelete: handleDeleteFile, - lng, - downloadingFileId - }); - - if (isLoading) { - return ( -
- - -
- ); - } - - return ( -
- {error && {error}} - - {/* 헤더 및 Sync 컨트롤 */} -
-
-

- Dolce Upload V3 Sync Enabled -

-
-
- -
-
- - {!projNo && {t("page.selectProject")}} - - {/* 필터 카드 */} - - {t("filter.title")} - -
-
- - -
- {/* 기타 필터들 */} -
setDrawingNo(e.target.value)} />
-
setDrawingName(e.target.value)} />
-
setDiscipline(e.target.value)} />
-
setManager(e.target.value)} />
- {vendorInfo?.drawingKind === "B4" && ( -
- - -
- )} -
-
- - {vendorInfo?.drawingKind === "B4" && ( - - )} - -
-
-
- - {/* 메인 컨텐츠 영역 */} - - {t("drawingList.title")} - - - - - -
- - - {t("detailDialog.detailListTitle")} -
- - {canAddDetailDrawing && } -
-
- - - -
- - - - {t("detailDialog.fileListTitle")} - {selectedDetail && canAddDetailDrawing && ( - - )} - - - - - -
- - {/* 다이얼로그 영역 */} - {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( - - )} - - {vendorInfo && selectedDrawing && ( - - )} - - {vendorInfo && selectedDetail && ( - - )} - - {/* 동기화 다이얼로그 */} - {vendorInfo && projNo && ( - - )} -
- ); -} diff --git a/app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx b/app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx deleted file mode 100644 index f62f486b..00000000 --- a/app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Suspense } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import DolceUploadPageV3 from "./dolce-upload-page-v3"; -import { Shell } from "@/components/shell"; - -export const metadata = { - title: "조선 벤더문서 업로드(DOLCE) V3", - description: "조선 설계문서 업로드 및 관리 - 오프라인 동기화 지원", -}; - -function DolceUploadSkeleton() { - return ( -
- - -
- ); -} - -export default async function DolceUploadPageWrapper({ - params, - searchParams, -}: { - params: Promise<{ lng: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - const { lng } = await params; - const resolvedParams = await searchParams; - - return ( - -
-
-

- {lng === "ko" ? "DOLCE 도면 업로드 V3 (동기화)" : "DOLCE Drawing Upload V3 (Sync)"} -

-

- {lng === "ko" ? "임시 저장 및 서버 동기화 기능을 지원합니다." : "Supports temporary save and server synchronization."} -

-
-
- - }> - - -
- ); -} diff --git a/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx b/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx deleted file mode 100644 index 1bb876fb..00000000 --- a/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx +++ /dev/null @@ -1,420 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useMemo } from "react"; -import { useParams } from "next/navigation"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { InfoIcon, RefreshCw, Search, Upload } from "lucide-react"; -import { toast } from "sonner"; -import { useTranslation } from "@/i18n/client"; -import { - UnifiedDwgReceiptItem, - fetchDwgReceiptList, - getVendorSessionInfo, - fetchVendorProjects, -} from "@/lib/dolce/actions"; -import { DrawingListTable } from "@/lib/dolce/table/drawing-list-table"; -import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; -import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; -import { DetailDrawingDialog } from "@/lib/dolce/dialogs/detail-drawing-dialog"; -// V2: MatchBatchFileDwg+MatchBatchFileDwgEdit API 사용, 별도의 RegisterKind 선택 없이 결과값 기준으로 업로드 -import { B4BulkUploadDialogV3 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v3"; -// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 -// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; - -interface DolceUploadPageProps { - searchParams: { [key: string]: string | string[] | undefined }; -} - -export default function DolceUploadPage({ searchParams }: DolceUploadPageProps) { - const params = useParams(); - const lng = params?.lng as string; - const { t } = useTranslation(lng, "dolce"); - - // URL에서 초기 프로젝트 코드 - const initialProjNo = (searchParams.projNo as string) || ""; - - // 상태 관리 - const [drawings, setDrawings] = useState([]); - const [projects, setProjects] = useState>([]); - const [vendorInfo, setVendorInfo] = useState<{ - userId: string; - userName: string; - email: string; - vendorCode: string; - vendorName: string; - drawingKind: "B3" | "B4"; - } | null>(null); - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [error, setError] = useState(null); - - // 필터 상태 - const [projNo, setProjNo] = useState(initialProjNo); - const [drawingNo, setDrawingNo] = useState(""); - const [drawingName, setDrawingName] = useState(""); - const [discipline, setDiscipline] = useState(""); - const [manager, setManager] = useState(""); - const [documentType, setDocumentType] = useState("ALL"); // B4 전용 - - // 선택된 도면 (다이얼로그용) - const [selectedDrawing, setSelectedDrawing] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - - // 일괄 업로드 다이얼로그 - const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); - - // 초기 데이터 로드 - const loadInitialData = useCallback(async () => { - try { - setIsLoading(true); - setError(null); - - // 병렬로 데이터 로드 - const [vendorInfoData, projectsData] = await Promise.all([ - getVendorSessionInfo(), - fetchVendorProjects(), - ]); - - setVendorInfo(vendorInfoData as typeof vendorInfo); - setProjects(projectsData); - - // 초기 프로젝트가 있으면 도면 로드 - if (initialProjNo) { - const drawingsData = await fetchDwgReceiptList({ - project: initialProjNo, - drawingKind: vendorInfoData.drawingKind, - drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "", - }); - setDrawings(drawingsData); - } - } catch (err) { - console.error("초기 데이터 로드 실패:", err); - setError(err instanceof Error ? err.message : t("page.initialLoadError")); - toast.error(t("page.initialLoadError")); - } finally { - setIsLoading(false); - } - }, [initialProjNo, t]); - - // 도면 목록 조회 - const loadDrawings = useCallback(async () => { - if (!projNo || !vendorInfo) return; - - try { - setIsRefreshing(true); - setError(null); - - const drawingsData = await fetchDwgReceiptList({ - project: projNo, - drawingKind: vendorInfo.drawingKind, - drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "", - }); - - setDrawings(drawingsData); - toast.success(t("page.drawingLoadSuccess")); - } catch (err) { - console.error("도면 로드 실패:", err); - setError(err instanceof Error ? err.message : t("page.drawingLoadError")); - toast.error(t("page.drawingLoadError")); - } finally { - setIsRefreshing(false); - } - }, [projNo, vendorInfo, t]); - - // 초기 데이터 로드 - useEffect(() => { - loadInitialData(); - }, [loadInitialData]); - - // 프로젝트 변경 시 자동 검색 - useEffect(() => { - if (projNo && vendorInfo) { - loadDrawings(); - } - }, [projNo, vendorInfo, loadDrawings]); - - // 도면 클릭 핸들러 - const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => { - setSelectedDrawing(drawing); - setDialogOpen(true); - }; - - // 검색 핸들러 - const handleSearch = () => { - loadDrawings(); - }; - - // 새로고침 핸들러 - const handleRefresh = () => { - loadDrawings(); - }; - - // 일괄 업로드 완료 핸들러 - const handleBulkUploadComplete = () => { - loadDrawings(); - }; - - // 필터된 도면 목록 (클라이언트 사이드 필터링) - const filteredDrawings = useMemo(() => { - let result = drawings.filter((drawing) => { - // 도면번호 필터 (공백 포함) - if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) { - return false; - } - - // 도면명 필터 (공백 포함) - if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) { - return false; - } - - // 설계공종 필터 (공백 포함) - if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) { - return false; - } - - // 담당자명 필터 (공백 포함) - if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) && - !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) { - return false; - } - - return true; - }); - - // B4인 경우 Document Type 필터 적용 - if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") { - result = result.filter((drawing) => { - // B4 타입 체크 - if (drawing.DrawingKind !== "B4") return false; - - // B4 도면의 DrawingMoveGbn 체크 - const gttDrawing = drawing as { DrawingMoveGbn?: string }; - - if (documentType === "SHI_INPUT") { - return gttDrawing.DrawingMoveGbn === "도면제출"; - } else if (documentType === "GTT_DELIVERABLES") { - return gttDrawing.DrawingMoveGbn === "도면입수"; - } - return true; - }); - } - - return result; - }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]); - - if (isLoading) { - return ( - - - - - - - - - - - ); - } - - return ( -
- {/* 에러 메시지 */} - {error && ( - - {error} - - )} - - {/* 안내 메시지 */} - {!projNo && ( - - - - {t("page.selectProject")} - - - )} - - {/* 필터 카드 */} - - - {t("filter.title")} - - -
- {/* 프로젝트 선택 */} -
- - -
- - {/* 도면번호 검색 */} -
- - setDrawingNo(e.target.value)} - placeholder={t("filter.drawingNoPlaceholder")} - /> -
- - {/* 도면명 검색 */} -
- - setDrawingName(e.target.value)} - placeholder={t("filter.drawingNamePlaceholder")} - /> -
- - {/* 설계공종 검색 */} -
- - setDiscipline(e.target.value)} - placeholder={t("filter.disciplinePlaceholder")} - /> -
- - {/* 담당자명 검색 (클라이언트 필터) */} -
- - setManager(e.target.value)} - placeholder={t("filter.managerPlaceholder")} - /> -
- - {/* B4(GTT) 전용: Document Type 필터 */} - {vendorInfo?.drawingKind === "B4" && ( -
- - -
- )} -
- -
- - {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */} - {vendorInfo?.drawingKind === "B4" && ( - - )} - -
-
-
- - {/* 도면 리스트 테이블 */} - {projNo && vendorInfo && ( - - - - {t("drawingList.title")} - {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`} - - - - - - - )} - - {/* 상세도면 다이얼로그 */} - {vendorInfo && ( - - )} - - {/* B4 일괄 업로드 다이얼로그 (V2) */} - {/* V2: MatchBatchFileDwg+MatchBatchFileDwgEdit API 사용, 별도의 RegisterKind 선택 없이 결과값 기준으로 업로드 */} - {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( - - )} - {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV2를 B4BulkUploadDialog로 변경하세요 */} -
- ); -} - diff --git a/app/[lng]/partners/(partners)/dolce-upload/page.tsx b/app/[lng]/partners/(partners)/dolce-upload/page.tsx deleted file mode 100644 index 4d7b1a74..00000000 --- a/app/[lng]/partners/(partners)/dolce-upload/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Suspense } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import DolceUploadPage from "./dolce-upload-page"; -import { Shell } from "@/components/shell"; - -export const metadata = { - title: "조선 벤더문서 업로드(DOLCE)", - description: "조선 설계문서 업로드 및 관리", -}; - -// ============================================================================ -// 로딩 스켈레톤 -// ============================================================================ - -function DolceUploadSkeleton() { - return ( - - -
- - -
-
- - - - -
- ); -} - -export default async function DolceUploadPageWrapper({ - params, - searchParams, -}: { - params: Promise<{ lng: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - const { lng } = await params; - const resolvedParams = await searchParams; - - return ( - - {/* 헤더 */} -
-
-

- {lng === "ko" - ? "DOLCE 도면 업로드" - : "DOLCE Drawing Upload"} -

-

- {lng === "ko" - ? "설계문서를 조회하고 업로드할 수 있습니다" - : "View and upload design documents"} -

-
-
- - {/* 메인 컨텐츠 */} - }> - - -
- ); -} \ No newline at end of file -- cgit v1.2.3