From fd4909bba7be8abc1eeab9ae1b4621c66a61604a Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Sun, 23 Nov 2025 16:40:37 +0900 Subject: (김준회) 돌체 재개발 - 1차 (다운로드 오류 수정 필요) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(partners)/dolce-upload/dolce-upload-page.tsx | 405 +++++++++++++++++++++ .../partners/(partners)/dolce-upload/page.tsx | 60 +++ app/api/dolce/download/route.ts | 43 +++ 3 files changed, 508 insertions(+) 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/api/dolce/download/route.ts (limited to 'app') 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..db8d528b --- /dev/null +++ b/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx @@ -0,0 +1,405 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +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 { + 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"; +import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; + +interface DolceUploadPageProps { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default function DolceUploadPage({ searchParams }: DolceUploadPageProps) { + // 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); + 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 : "데이터 로드 실패"); + toast.error("데이터 로드 실패"); + } finally { + setIsLoading(false); + } + }, [initialProjNo]); + + // 도면 목록 조회 + 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("도면 목록을 갱신했습니다"); + } catch (err) { + console.error("도면 로드 실패:", err); + setError(err instanceof Error ? err.message : "도면 로드 실패"); + toast.error("도면 로드 실패"); + } finally { + setIsRefreshing(false); + } + }, [projNo, vendorInfo]); + + // 초기 데이터 로드 + useEffect(() => { + loadInitialData(); + }, [loadInitialData]); + + // 도면 클릭 핸들러 + 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 && ( + + + + 프로젝트를 선택하여 도면 목록을 조회하세요. + + + )} + + {/* 필터 카드 */} + + + 검색 필터 + + +
+ {/* 프로젝트 선택 */} +
+ + +
+ + {/* 도면번호 검색 */} +
+ + setDrawingNo(e.target.value)} + placeholder="도면번호 입력" + /> +
+ + {/* 도면명 검색 */} +
+ + setDrawingName(e.target.value)} + placeholder="도면명 입력" + /> +
+ + {/* 설계공종 검색 */} +
+ + setDiscipline(e.target.value)} + placeholder="설계공종 입력" + /> +
+ + {/* 담당자명 검색 (클라이언트 필터) */} +
+ + setManager(e.target.value)} + placeholder="담당자명 입력" + /> +
+ + {/* B4(GTT) 전용: Document Type 필터 */} + {vendorInfo?.drawingKind === "B4" && ( +
+ + +
+ )} +
+ +
+ + + {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */} + {vendorInfo?.drawingKind === "B4" && ( + + )} +
+
+
+ + {/* 도면 리스트 테이블 */} + {projNo && vendorInfo && ( + + + + 도면 리스트 + {filteredDrawings.length > 0 && ` (${filteredDrawings.length}건)`} + + + + {vendorInfo.drawingKind === "B4" ? ( + + ) : ( + + )} + + + )} + + {/* 상세도면 다이얼로그 */} + {vendorInfo && ( + + )} + + {/* B4 일괄 업로드 다이얼로그 */} + {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( + + )} +
+ ); +} + 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..d44e71b6 --- /dev/null +++ b/app/[lng]/partners/(partners)/dolce-upload/page.tsx @@ -0,0 +1,60 @@ +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({ + searchParams, +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const params = await searchParams; + + return ( + + {/* 헤더 */} +
+
+

+ DOLCE 문서 업로드 +

+

+ 설계문서를 조회하고 업로드할 수 있습니다 +

+
+
+ + {/* 메인 컨텐츠 */} + }> + + +
+ ); +} \ No newline at end of file diff --git a/app/api/dolce/download/route.ts b/app/api/dolce/download/route.ts new file mode 100644 index 00000000..9d5eb601 --- /dev/null +++ b/app/api/dolce/download/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from "next/server"; +import { downloadDolceFile } from "@/lib/dolce/actions"; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { fileId, userId, fileName } = body; + + if (!fileId || !userId || !fileName) { + return NextResponse.json( + { error: "필수 파라미터가 누락되었습니다" }, + { status: 400 } + ); + } + + console.log("[DOLCE Download API] 요청:", { fileId, userId, fileName }); + + // DOLCE API를 통해 파일 다운로드 + const { blob, fileName: downloadFileName } = await downloadDolceFile({ + fileId, + userId, + fileName, + }); + + // ArrayBuffer로 변환하여 Response 생성 + const arrayBuffer = await blob.arrayBuffer(); + + return new NextResponse(arrayBuffer, { + headers: { + "Content-Type": "application/octet-stream", + "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(downloadFileName)}`, + "Content-Length": arrayBuffer.byteLength.toString(), + }, + }); + } catch (error) { + console.error("파일 다운로드 API 오류:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "파일 다운로드 중 오류가 발생했습니다" }, + { status: 500 } + ); + } +} + -- cgit v1.2.3