From 25b2561bf17128b96f023c977efb5cb51da0b4aa Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 24 Nov 2025 22:41:52 +0900 Subject: (김준회) dolce: 기존 레이아웃과 유사한 v2 추가, bulk-upload를 MatchBatchDwgFile 사용하지 않도록 변경 (해당 API 동작 이상함) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dolce-upload-v2/dolce-upload-page-v2.tsx | 719 +++++++++++++++++++++ .../partners/(partners)/dolce-upload-v2/page.tsx | 74 +++ .../(partners)/dolce-upload/dolce-upload-page.tsx | 11 +- 3 files changed, 801 insertions(+), 3 deletions(-) create mode 100644 app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx create mode 100644 app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx (limited to 'app') 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 new file mode 100644 index 00000000..79f1b147 --- /dev/null +++ b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx @@ -0,0 +1,719 @@ +"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"; +// V2: MatchBatchFileDwg API를 사용하지 않는 새로운 일괄 업로드 (DetailDwgReceiptMgmtEdit 사용) +import { B4BulkUploadDialogV2 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v2"; +// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 +// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; +import { AddDetailDrawingDialog } from "@/lib/dolce/dialogs/add-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} + maxHeight="calc(100vh - 600px)" + minHeight="400px" + defaultPageSize={10} + /> + )} +
+
+ + {/* 우측: 첨부파일 리스트 */} + + + + {t("detailDialog.fileListTitle")} + {selectedDetail && ( + + Rev. {selectedDetail.DrawingRevNo} + + )} + + {selectedDetail && canAddDetailDrawing && ( + + )} + + + {!selectedDetail ? ( +
+
+ +

{t("detailDialog.selectDetailDrawing")}

+
+
+ ) : isLoadingFiles ? ( +
+ +
+ ) : ( + + )} +
+
+
+ + {/* B4 일괄 업로드 다이얼로그 (V2) */} + {/* V2: MatchBatchFileDwg API를 사용하지 않는 새로운 방식 */} + {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( + + )} + {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV2를 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 new file mode 100644 index 00000000..6655606f --- /dev/null +++ b/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx @@ -0,0 +1,74 @@ +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" + ? "DOLCE 도면 업로드 V2" + : "DOLCE Drawing Upload V2"} +

+

+ {lng === "ko" + ? "설계문서를 조회하고 업로드할 수 있습니다 (분할 레이아웃)" + : "View and upload design documents (Split Layout)"} +

+
+
+ + {/* 메인 컨텐츠 */} + }> + + +
+ ); +} + diff --git a/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx b/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx index 43800838..e03f6bc2 100644 --- a/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx +++ b/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx @@ -28,7 +28,10 @@ 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"; -import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; +// V2: MatchBatchFileDwg API를 사용하지 않는 새로운 일괄 업로드 (DetailDwgReceiptMgmtEdit 사용) +import { B4BulkUploadDialogV2 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v2"; +// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 +// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; interface DolceUploadPageProps { searchParams: { [key: string]: string | string[] | undefined }; @@ -395,9 +398,10 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps) /> )} - {/* B4 일괄 업로드 다이얼로그 */} + {/* B4 일괄 업로드 다이얼로그 (V2) */} + {/* V2: MatchBatchFileDwg API를 사용하지 않는 새로운 방식 */} {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( - )} + {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV2를 B4BulkUploadDialog로 변경하세요 */} ); } -- cgit v1.2.3