summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-23 16:40:37 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-23 16:40:37 +0900
commitfd4909bba7be8abc1eeab9ae1b4621c66a61604a (patch)
treed375995611de80b55b344b1c536c5a760f06ccb6 /app
parenta2e0785c8749c4d3766ecf3b70edfb7c2fe4df20 (diff)
(김준회) 돌체 재개발 - 1차 (다운로드 오류 수정 필요)
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx405
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload/page.tsx60
-rw-r--r--app/api/dolce/download/route.ts43
3 files changed, 508 insertions, 0 deletions
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<UnifiedDwgReceiptItem[]>([]);
+ const [projects, setProjects] = useState<Array<{ code: string; name: string }>>([]);
+ 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<string | null>(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<DocumentType>("ALL"); // B4 전용
+
+ // 선택된 도면 (다이얼로그용)
+ const [selectedDrawing, setSelectedDrawing] = useState<UnifiedDwgReceiptItem | null>(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 (
+ <Card>
+ <CardHeader>
+ <Skeleton className="h-8 w-48" />
+ <Skeleton className="h-4 w-96" />
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <Skeleton className="h-32 w-full" />
+ <Skeleton className="h-96 w-full" />
+ </CardContent>
+ </Card>
+ );
+ }
+
+ return (
+ <div className="space-y-6 max-w-full overflow-x-hidden">
+ {/* 에러 메시지 */}
+ {error && (
+ <Alert variant="destructive">
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ )}
+
+ {/* 안내 메시지 */}
+ {!projNo && (
+ <Alert>
+ <InfoIcon className="h-4 w-4" />
+ <AlertDescription>
+ 프로젝트를 선택하여 도면 목록을 조회하세요.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* 필터 카드 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>검색 필터</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {/* 프로젝트 선택 */}
+ <div className="space-y-2">
+ <Label>프로젝트</Label>
+ <Select value={projNo} onValueChange={setProjNo}>
+ <SelectTrigger>
+ <SelectValue placeholder="프로젝트를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ {projects.map((project) => (
+ <SelectItem key={project.code} value={project.code}>
+ {project.code} - {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+
+ {/* 도면번호 검색 */}
+ <div className="space-y-2">
+ <Label>도면번호 (Drawing No)</Label>
+ <Input
+ value={drawingNo}
+ onChange={(e) => setDrawingNo(e.target.value)}
+ placeholder="도면번호 입력"
+ />
+ </div>
+
+ {/* 도면명 검색 */}
+ <div className="space-y-2">
+ <Label>도면명 (Drawing Name)</Label>
+ <Input
+ value={drawingName}
+ onChange={(e) => setDrawingName(e.target.value)}
+ placeholder="도면명 입력"
+ />
+ </div>
+
+ {/* 설계공종 검색 */}
+ <div className="space-y-2">
+ <Label>설계공종 (Discipline)</Label>
+ <Input
+ value={discipline}
+ onChange={(e) => setDiscipline(e.target.value)}
+ placeholder="설계공종 입력"
+ />
+ </div>
+
+ {/* 담당자명 검색 (클라이언트 필터) */}
+ <div className="space-y-2">
+ <Label>담당자명 (Manager)</Label>
+ <Input
+ value={manager}
+ onChange={(e) => setManager(e.target.value)}
+ placeholder="담당자명 입력"
+ />
+ </div>
+
+ {/* B4(GTT) 전용: Document Type 필터 */}
+ {vendorInfo?.drawingKind === "B4" && (
+ <div className="space-y-2">
+ <Label>Document Type</Label>
+ <Select value={documentType} onValueChange={(value) => setDocumentType(value as DocumentType)}>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="ALL">ALL (전체)</SelectItem>
+ <SelectItem value="GTT_DELIVERABLES">GTT Deliverables (도면입수)</SelectItem>
+ <SelectItem value="SHI_INPUT">SHI Input Document (도면제출)</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+ </div>
+
+ <div className="flex gap-2 mt-4">
+ <Button
+ onClick={handleSearch}
+ disabled={!projNo || isRefreshing}
+ className="flex-1"
+ >
+ <Search className="h-4 w-4 mr-2" />
+ 검색
+ </Button>
+ <Button
+ variant="outline"
+ onClick={handleRefresh}
+ disabled={!projNo || isRefreshing}
+ >
+ <RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
+ </Button>
+ {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */}
+ {vendorInfo?.drawingKind === "B4" && (
+ <Button
+ variant="default"
+ onClick={() => setBulkUploadDialogOpen(true)}
+ disabled={!projNo || isRefreshing}
+ >
+ <Upload className="h-4 w-4 mr-2" />
+ 일괄 업로드
+ </Button>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 도면 리스트 테이블 */}
+ {projNo && vendorInfo && (
+ <Card>
+ <CardHeader>
+ <CardTitle>
+ 도면 리스트
+ {filteredDrawings.length > 0 && ` (${filteredDrawings.length}건)`}
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="overflow-x-auto">
+ {vendorInfo.drawingKind === "B4" ? (
+ <DrawingListTable
+ columns={createGttDrawingListColumns({ documentType })}
+ data={filteredDrawings}
+ onRowClick={handleDrawingClick}
+ />
+ ) : (
+ <DrawingListTable
+ columns={drawingListColumns}
+ data={filteredDrawings}
+ onRowClick={handleDrawingClick}
+ />
+ )}
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 상세도면 다이얼로그 */}
+ {vendorInfo && (
+ <DetailDrawingDialog
+ drawing={selectedDrawing}
+ open={dialogOpen}
+ onOpenChange={setDialogOpen}
+ vendorCode={vendorInfo.vendorCode}
+ userId={vendorInfo.userId}
+ userName={vendorInfo.userName}
+ userEmail={vendorInfo.email}
+ drawingKind={vendorInfo.drawingKind}
+ />
+ )}
+
+ {/* B4 일괄 업로드 다이얼로그 */}
+ {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && (
+ <B4BulkUploadDialog
+ open={bulkUploadDialogOpen}
+ onOpenChange={setBulkUploadDialogOpen}
+ projectNo={projNo}
+ userId={vendorInfo.userId}
+ userName={vendorInfo.userName}
+ userEmail={vendorInfo.email}
+ vendorCode={vendorInfo.vendorCode}
+ onUploadComplete={handleBulkUploadComplete}
+ />
+ )}
+ </div>
+ );
+}
+
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 (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <Skeleton className="h-8 w-32" />
+ <Skeleton className="h-10 w-40" />
+ </div>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <Skeleton className="h-32 w-full" />
+ <Skeleton className="h-96 w-full" />
+ </CardContent>
+ </Card>
+ );
+}
+
+export default async function DolceUploadPageWrapper({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const params = await searchParams;
+
+ return (
+ <Shell>
+ {/* 헤더 */}
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ DOLCE 문서 업로드
+ </h2>
+ <p className="text-muted-foreground">
+ 설계문서를 조회하고 업로드할 수 있습니다
+ </p>
+ </div>
+ </div>
+
+ {/* 메인 컨텐츠 */}
+ <Suspense fallback={<DolceUploadSkeleton />}>
+ <DolceUploadPage searchParams={params} />
+ </Suspense>
+ </Shell>
+ );
+} \ 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 }
+ );
+ }
+}
+