summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx4
-rw-r--r--lib/dolce/actions.ts258
-rw-r--r--lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx (renamed from lib/dolce/dialogs/add-detail-drawing-dialog.tsx)381
-rw-r--r--lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx258
-rw-r--r--lib/dolce/dialogs/b4-bulk-upload-dialog.tsx108
-rw-r--r--lib/dolce/dialogs/b4-upload-validation-dialog.tsx99
-rw-r--r--lib/dolce/dialogs/detail-drawing-dialog.tsx35
-rw-r--r--lib/dolce/dialogs/upload-files-to-detail-dialog.tsx35
-rw-r--r--lib/dolce/table/detail-drawing-columns.tsx112
9 files changed, 976 insertions, 314 deletions
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
index 79f1b147..f5337c1c 100644
--- 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
@@ -37,7 +37,7 @@ import { createFileListColumns } from "@/lib/dolce/table/file-list-columns";
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 { AddAndModifyDetailDrawingDialog } from "@/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog";
import { UploadFilesToDetailDialog } from "@/lib/dolce/dialogs/upload-files-to-detail-dialog";
interface DolceUploadPageV2Props {
@@ -686,7 +686,7 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro
{/* 상세도면 추가 다이얼로그 */}
{vendorInfo && selectedDrawing && (
- <AddDetailDrawingDialog
+ <AddAndModifyDetailDrawingDialog
open={addDialogOpen}
onOpenChange={setAddDialogOpen}
drawing={selectedDrawing}
diff --git a/lib/dolce/actions.ts b/lib/dolce/actions.ts
index 552a9a6a..8c5dfa1b 100644
--- a/lib/dolce/actions.ts
+++ b/lib/dolce/actions.ts
@@ -172,7 +172,7 @@ async function dolceApiCall<T>(endpoint: string, body: Record<string, unknown>):
});
if (!response.ok) {
- throw new Error(`DOLCE API 오류 (${endpoint}): ${response.status} ${response.statusText}`);
+ throw new Error(`DOLCE API error (${endpoint}): ${response.status} ${response.statusText}`);
}
const data = await response.json();
@@ -356,7 +356,7 @@ export async function downloadDolceFile(params: {
if (!response.ok) {
const errorText = await response.text();
console.error("[DOLCE] 에러 응답:", errorText);
- throw new Error(`파일 다운로드 실패: ${response.status} - ${errorText}`);
+ throw new Error(`File download failed: ${response.status} - ${errorText}`);
}
// HTML/텍스트 응답인 경우 에러일 수 있음
@@ -364,7 +364,7 @@ export async function downloadDolceFile(params: {
if (contentType.includes("text") || contentType.includes("html")) {
const errorText = await response.text();
console.error("[DOLCE] 텍스트 응답 (에러):", errorText);
- throw new Error(`예상치 못한 응답: ${errorText.substring(0, 200)}`);
+ throw new Error(`Unexpected response: ${errorText.substring(0, 200)}`);
}
const blob = await response.blob();
@@ -387,7 +387,7 @@ export async function getVendorSessionInfo() {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
- throw new Error("로그인이 필요합니다");
+ throw new Error("Login required");
}
// DB에서 사용자 정보 조회
@@ -403,7 +403,7 @@ export async function getVendorSessionInfo() {
});
if (!userInfo || !userInfo.companyId) {
- throw new Error("벤더 정보를 찾을 수 없습니다");
+ throw new Error("Vendor information not found");
}
// 벤더 정보 조회
@@ -417,7 +417,7 @@ export async function getVendorSessionInfo() {
});
if (!vendorInfo) {
- throw new Error("벤더 정보를 찾을 수 없습니다");
+ throw new Error("Vendor information not found");
}
// GTT 벤더 확인 (A0016193)
@@ -582,7 +582,7 @@ export async function uploadFilesToDetailDrawing(
const fileCount = parseInt(formData.get("fileCount") as string);
if (!uploadId || !userId || !fileCount) {
- throw new Error("필수 파라미터가 누락되었습니다");
+ throw new Error("Required parameters are missing");
}
const uploadResults: Array<{
@@ -629,7 +629,7 @@ export async function uploadFilesToDetailDrawing(
if (!uploadResponse.ok) {
throw new Error(
- `파일 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`
+ `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`
);
}
@@ -661,13 +661,13 @@ export async function uploadFilesToDetailDrawing(
if (!resultResponse.ok) {
throw new Error(
- `업로드 완료 통지 실패: ${resultResponse.status} ${resultResponse.statusText}`
+ `Upload notification failed: ${resultResponse.status} ${resultResponse.statusText}`
);
}
const resultText = await resultResponse.text();
if (resultText !== "Success") {
- throw new Error(`업로드 완료 통지 실패: ${resultText}`);
+ throw new Error(`Upload notification failed: ${resultText}`);
}
return {
@@ -678,7 +678,7 @@ export async function uploadFilesToDetailDrawing(
console.error("파일 업로드 실패:", error);
return {
success: false,
- error: error instanceof Error ? error.message : "알 수 없는 오류",
+ error: error instanceof Error ? error.message : "Unknown error",
};
}
}
@@ -747,7 +747,7 @@ async function parseB4FileName(fileName: string): Promise<{
try {
const lastDotIndex = fileName.lastIndexOf(".");
if (lastDotIndex === -1) {
- return { valid: false, error: "파일 확장자가 없습니다" };
+ return { valid: false, error: "File extension is missing" };
}
const nameWithoutExt = fileName.substring(0, lastDotIndex);
@@ -756,7 +756,7 @@ async function parseB4FileName(fileName: string): Promise<{
if (parts.length < 3) {
return {
valid: false,
- error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개)`,
+ error: `At least 2 spaces required (current: ${parts.length - 1})`,
};
}
@@ -765,30 +765,212 @@ async function parseB4FileName(fileName: string): Promise<{
const drawingNo = drawingTokens.join("-");
if (!drawingNo || !revNo) {
- return { valid: false, error: "도면번호 또는 리비전번호가 비어있습니다" };
+ return { valid: false, error: "Drawing number or revision number is empty" };
}
return { valid: true, drawingNo: drawingNo.trim(), revNo: revNo.trim() };
} catch (error) {
return {
valid: false,
- error: error instanceof Error ? error.message : "알 수 없는 오류",
+ error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
- * B4 파일 일괄 업로드 V2
+ * B4 일괄 업로드를 위한 상세도면 준비 (V2)
*
- * MatchBatchFileDwg/MatchBatchFileDwgEdit API 대신
- * DetailDwgReceiptMgmtEdit API와 업로드 서비스만 사용
+ * 반환값: DrawingNo + RevNo별 UploadId 정보
+ * 파일 업로드는 클라이언트에서 직접 처리
+ */
+export interface B4DetailDrawingInfo {
+ drawingNo: string;
+ revNo: string;
+ uploadId: string;
+ isNew: boolean; // 새로 생성된 상세도면인지 여부
+ drawingName?: string;
+ discipline?: string;
+}
+
+export async function prepareB4DetailDrawingsV2(params: {
+ projectNo: string;
+ userId: string;
+ userNm: string;
+ email: string;
+ vendorCode: string;
+ registerKind: string;
+ drawingRevisions: Array<{ drawingNo: string; revNo: string }>;
+}): Promise<{
+ success: boolean;
+ detailDrawings?: B4DetailDrawingInfo[];
+ error?: string;
+}> {
+ try {
+ console.log("[V2 Prepare] 상세도면 준비 시작");
+
+ const { projectNo, userId, userNm, email, vendorCode, registerKind, drawingRevisions } = params;
+
+ const detailDrawings: B4DetailDrawingInfo[] = [];
+
+ // 중복 제거: 동일한 DrawingNo + RevNo 조합은 1번만 처리
+ const uniqueRevisions = Array.from(
+ new Map(
+ drawingRevisions.map((r) => [`${r.drawingNo}_${r.revNo}`, r])
+ ).values()
+ );
+
+ if (uniqueRevisions.length !== drawingRevisions.length) {
+ console.warn(
+ `[V2 Prepare] 중복 제거: ${drawingRevisions.length}개 → ${uniqueRevisions.length}개`
+ );
+ }
+
+ console.log(`[V2 Prepare] 처리할 리비전: ${uniqueRevisions.length}개`);
+
+ // DrawingNo별로 그룹화
+ const drawingNoSet = new Set(uniqueRevisions.map((r) => r.drawingNo));
+ const drawingInfoMap = new Map<string, GttDwgReceiptItem>();
+
+ // 1. 기본 도면 정보 조회
+ for (const drawingNo of drawingNoSet) {
+ try {
+ const dwgList = await fetchDwgReceiptList({
+ project: projectNo,
+ drawingKind: "B4",
+ drawingMoveGbn: "도면입수",
+ drawingNo: drawingNo,
+ });
+
+ const dwgInfo = dwgList.find(
+ (d) => (d as GttDwgReceiptItem).DrawingNo === drawingNo
+ ) as GttDwgReceiptItem | undefined;
+
+ if (dwgInfo) {
+ drawingInfoMap.set(drawingNo, dwgInfo);
+ }
+ } catch (error) {
+ console.error(`[V2 Prepare] 도면 정보 조회 실패: ${drawingNo}`, error);
+ }
+ }
+
+ // 2. 각 RevNo별로 상세도면 확인/생성 (중복 제거된 리스트 사용)
+ for (const { drawingNo, revNo } of uniqueRevisions) {
+ try {
+ const drawingInfo = drawingInfoMap.get(drawingNo);
+ if (!drawingInfo) {
+ throw new Error(`Drawing information not found: ${drawingNo}`);
+ }
+
+ console.log(`[V2 Prepare] 처리 중: ${drawingNo} Rev.${revNo}`);
+
+ // 기존 상세도면 조회
+ const detailDwgList = await fetchDetailDwgReceiptList({
+ project: projectNo,
+ drawingNo: drawingNo,
+ discipline: drawingInfo.Discipline,
+ drawingKind: "B4",
+ userId: userId,
+ });
+
+ // 해당 RevNo의 상세도면 찾기
+ const existingDetail = detailDwgList.find(
+ (d) => d.DrawingRevNo === revNo
+ );
+
+ let uploadId: string;
+ let isNew = false;
+
+ if (existingDetail) {
+ // 1. 기존 상세도면이 있는 경우: 해당 uploadId 재사용
+ uploadId = existingDetail.UploadId;
+ isNew = false;
+ console.log(
+ `[V2 Prepare] ✓ 기존 상세도면 재사용: ${drawingNo} Rev.${revNo}, UploadId: ${uploadId}`
+ );
+ } else {
+ // 2. 상세도면이 없는 경우: 새로 1번만 생성
+ uploadId = crypto.randomUUID();
+ isNew = true;
+ console.log(
+ `[V2 Prepare] ✓ 새 상세도면 생성: ${drawingNo} Rev.${revNo}, UploadId: ${uploadId}`
+ );
+
+ const category = detailDwgList.length > 0 ? detailDwgList[0].Category : "NORM";
+
+ const addRequest: DetailDwgEditRequest = {
+ Mode: "ADD",
+ Status: "01",
+ RegisterId: 0,
+ ProjectNo: projectNo,
+ Discipline: drawingInfo.Discipline,
+ DrawingKind: "B4",
+ DrawingNo: drawingNo,
+ DrawingName: drawingInfo.DrawingName,
+ RegisterGroupId: drawingInfo.RegisterGroupId,
+ RegisterSerialNo: drawingInfo.RegisterGroup,
+ RegisterKind: registerKind,
+ DrawingRevNo: revNo,
+ Category: category,
+ Receiver: null,
+ Manager: drawingInfo.Manager || "970043",
+ RegisterDesc: "",
+ UploadId: uploadId,
+ RegCompanyCode: vendorCode,
+ };
+
+ await editDetailDwgReceipt({
+ dwgList: [addRequest],
+ userId: userId,
+ userNm: userNm,
+ vendorCode: vendorCode,
+ email: email,
+ });
+
+ console.log(`[V2 Prepare] ✓ DetailDwgReceiptMgmtEdit 호출 완료`);
+ }
+
+ detailDrawings.push({
+ drawingNo,
+ revNo,
+ uploadId,
+ isNew,
+ drawingName: drawingInfo.DrawingName,
+ discipline: drawingInfo.Discipline,
+ });
+ } catch (error) {
+ console.error(
+ `[V2 Prepare] ✗ 상세도면 준비 실패: ${drawingNo} Rev.${revNo}`,
+ error
+ );
+ throw error;
+ }
+ }
+
+ const newCount = detailDrawings.filter((d) => d.isNew).length;
+ const existingCount = detailDrawings.length - newCount;
+
+ console.log(
+ `[V2 Prepare] ✓ 완료: 총 ${detailDrawings.length}개 (신규: ${newCount}개, 기존: ${existingCount}개)`
+ );
+
+ return {
+ success: true,
+ detailDrawings,
+ };
+ } catch (error) {
+ console.error("[V2 Prepare] 실패:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ };
+ }
+}
+
+/**
+ * B4 파일 일괄 업로드 V2 (레거시 - 사용 안 함)
*
- * 프로세스:
- * 1. 파일명 파싱하여 DrawingNo, RevNo 추출
- * 2. 기존 도면 정보 조회 (fetchDwgReceiptList)
- * 3. 기존 상세도면 조회 (fetchDetailDwgReceiptList)
- * 4. 없으면 ADD, 있으면 기존 UploadId 사용
- * 5. 파일 업로드 (/api/dolce/upload-files)
+ * @deprecated 서버 액션에서 fetch 호출 시 상대 경로 문제로 사용 중단
+ * 대신 prepareB4DetailDrawingsV2 + 클라이언트 uploadFilesWithProgress 사용
*/
export async function bulkUploadB4FilesV2(
formData: FormData
@@ -806,7 +988,7 @@ export async function bulkUploadB4FilesV2(
const fileCount = parseInt(formData.get("fileCount") as string);
if (!projectNo || !userId || !userNm || !email || !vendorCode || !registerKind || !fileCount) {
- throw new Error("필수 파라미터가 누락되었습니다");
+ throw new Error("Required parameters are missing");
}
console.log(`[V2] 프로젝트: ${projectNo}, 사용자: ${userId}, 파일 수: ${fileCount}`);
@@ -843,7 +1025,7 @@ export async function bulkUploadB4FilesV2(
revNo: "",
fileName: file.name,
success: false,
- error: parseResult.error || "파일명 파싱 실패",
+ error: parseResult.error || "Failed to parse filename",
});
failCount++;
continue;
@@ -922,7 +1104,7 @@ export async function bulkUploadB4FilesV2(
// 도면 정보가 없으면 실패
if (!drawingInfo) {
- throw new Error(`도면 정보를 찾을 수 없습니다: ${drawingNo}`);
+ throw new Error(`Drawing information not found: ${drawingNo}`);
}
// 4-1. 기존 상세도면 조회
@@ -1009,13 +1191,13 @@ export async function bulkUploadB4FilesV2(
if (!uploadResponse.ok) {
const errorData = await uploadResponse.json();
- throw new Error(errorData.error || `파일 업로드 실패: ${uploadResponse.status}`);
+ throw new Error(errorData.error || `File upload failed: ${uploadResponse.status}`);
}
const uploadResult = await uploadResponse.json();
if (!uploadResult.success) {
- throw new Error(uploadResult.error || "파일 업로드 실패");
+ throw new Error(uploadResult.error || "File upload failed");
}
console.log(`[V2] 파일 업로드 완료: ${groupKey}`);
@@ -1033,7 +1215,7 @@ export async function bulkUploadB4FilesV2(
} catch (error) {
// 실패 처리
const errorMessage =
- error instanceof Error ? error.message : "알 수 없는 오류";
+ error instanceof Error ? error.message : "Unknown error";
console.error(`[V2] 그룹 처리 실패: ${groupKey}`, error);
@@ -1062,7 +1244,7 @@ export async function bulkUploadB4FilesV2(
console.error("[V2] 일괄 업로드 실패:", error);
return {
success: false,
- error: error instanceof Error ? error.message : "알 수 없는 오류",
+ error: error instanceof Error ? error.message : "Unknown error",
};
}
}
@@ -1082,7 +1264,7 @@ export async function bulkUploadB4Files(
const registerKind = formData.get("registerKind") as string;
if (!projectNo || !userId || !fileCount || !registerKind) {
- throw new Error("필수 파라미터가 누락되었습니다");
+ throw new Error("Required parameters are missing");
}
const results: Array<{
@@ -1176,7 +1358,7 @@ export async function bulkUploadB4Files(
if (!uploadResponse.ok) {
throw new Error(
- `파일 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`
+ `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`
);
}
@@ -1208,13 +1390,13 @@ export async function bulkUploadB4Files(
if (!resultResponse.ok) {
throw new Error(
- `업로드 완료 통지 실패: ${resultResponse.status} ${resultResponse.statusText}`
+ `Upload notification failed: ${resultResponse.status} ${resultResponse.statusText}`
);
}
const resultText = await resultResponse.text();
if (resultText !== "Success") {
- throw new Error(`업로드 완료 통지 실패: ${resultText}`);
+ throw new Error(`Upload notification failed: ${resultText}`);
}
// 4. 매핑 현황 재조회 (MatchBatchFileDwg)
@@ -1228,7 +1410,7 @@ export async function bulkUploadB4Files(
const mappingData = mappingCheckResults[0];
if (!mappingData || mappingData.RegisterGroupId === 0) {
- throw new Error("매핑 정보를 찾을 수 없습니다");
+ throw new Error("Mapping information not found");
}
// 5. 매핑 정보 저장 (MatchBatchFileDwgEdit)
@@ -1278,7 +1460,7 @@ export async function bulkUploadB4Files(
} catch (error) {
// 실패 처리
const errorMessage =
- error instanceof Error ? error.message : "알 수 없는 오류";
+ error instanceof Error ? error.message : "Unknown error";
for (const fileInfo of files) {
results.push({
@@ -1303,7 +1485,7 @@ export async function bulkUploadB4Files(
console.error("일괄 업로드 실패:", error);
return {
success: false,
- error: error instanceof Error ? error.message : "알 수 없는 오류",
+ error: error instanceof Error ? error.message : "Unknown error",
};
}
}
diff --git a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx
index 48614ecf..87819693 100644
--- a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
+++ b/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
@@ -19,9 +19,11 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Textarea } from "@/components/ui/textarea";
import { Upload, X, FileIcon, Info } from "lucide-react";
import { toast } from "sonner";
-import { UnifiedDwgReceiptItem, editDetailDwgReceipt } from "../actions";
+import { useTranslation } from "@/i18n/client";
+import { UnifiedDwgReceiptItem, DetailDwgReceiptItem, editDetailDwgReceipt } from "../actions";
import { v4 as uuidv4 } from "uuid";
import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress";
import { uploadFilesWithProgress } from "../utils/upload-with-progress";
@@ -33,7 +35,7 @@ import {
getB4RegisterKindOptions
} from "../utils/code-translator";
-interface AddDetailDrawingDialogProps {
+interface AddAndModifyDetailDrawingDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
drawing: UnifiedDwgReceiptItem | null;
@@ -43,10 +45,12 @@ interface AddDetailDrawingDialogProps {
userEmail: string;
onComplete: () => void;
drawingKind: "B3" | "B4";
- lng?: string; // i18n support
+ lng: string;
+ mode?: "add" | "edit";
+ detailDrawing?: DetailDwgReceiptItem | null;
}
-export function AddDetailDrawingDialog({
+export function AddAndModifyDetailDrawingDialog({
open,
onOpenChange,
drawing,
@@ -56,14 +60,31 @@ export function AddDetailDrawingDialog({
userEmail,
onComplete,
drawingKind,
- lng = "ko",
-}: AddDetailDrawingDialogProps) {
+ lng,
+ mode = "add",
+ detailDrawing = null,
+}: AddAndModifyDetailDrawingDialogProps) {
+ const { t } = useTranslation(lng, "dolce");
const [drawingUsage, setDrawingUsage] = useState<string>("");
const [registerKind, setRegisterKind] = useState<string>("");
const [revision, setRevision] = useState<string>("");
const [revisionError, setRevisionError] = useState<string>("");
+ const [comment, setComment] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Edit 모드일 때 초기값 설정
+ useEffect(() => {
+ if (mode === "edit" && detailDrawing && open) {
+ setDrawingUsage(detailDrawing.DrawingUsage || "");
+ setRegisterKind(detailDrawing.RegisterKind || "");
+ setRevision(detailDrawing.DrawingRevNo || "");
+ setComment(detailDrawing.RegisterDesc || "");
+ } else if (mode === "add" && open) {
+ // Add 모드로 열릴 때는 초기화
+ resetForm();
+ }
+ }, [mode, detailDrawing, open]);
+
// 옵션 생성 (다국어 지원)
const drawingUsageOptions = drawingKind === "B3"
? getB3DrawingUsageOptions(lng)
@@ -94,7 +115,7 @@ export function AddDetailDrawingDialog({
// Revision 유효성 검증 함수
const validateRevision = (value: string): string => {
if (!value.trim()) {
- return "Revision을 입력하세요";
+ return t("addDetailDialog.revisionRequired");
}
const upperValue = value.toUpperCase().trim();
@@ -109,7 +130,7 @@ export function AddDetailDrawingDialog({
return "";
}
- return "올바른 형식이 아닙니다 (A-Z 또는 R00-R99)";
+ return t("addDetailDialog.revisionInvalidFormat");
};
// Revision 입력 핸들러
@@ -132,25 +153,20 @@ export function AddDetailDrawingDialog({
setRegisterKind("");
setRevision("");
setRevisionError("");
+ setComment("");
clearFiles();
};
// 제출
const handleSubmit = async () => {
- if (!drawing) return;
-
// 유효성 검사
- if (!drawingUsage) {
- toast.error("도면용도를 선택하세요");
- return;
- }
if (!registerKind) {
- toast.error("등록종류를 선택하세요");
+ toast.error(t("addDetailDialog.selectRegisterKindError"));
return;
}
if (!revision.trim()) {
- toast.error("Revision을 입력하세요");
- setRevisionError("Revision을 입력하세요");
+ toast.error(t("addDetailDialog.selectRevisionError"));
+ setRevisionError(t("addDetailDialog.revisionRequired"));
return;
}
@@ -162,93 +178,148 @@ export function AddDetailDrawingDialog({
return;
}
- if (files.length === 0) {
- toast.error("최소 1개 이상의 파일을 첨부해야 합니다");
+ // Add 모드일 때만 파일 필수
+ if (mode === "add") {
+ if (!drawing) return;
+ if (!drawingUsage) {
+ toast.error(t("addDetailDialog.selectDrawingUsageError"));
+ return;
+ }
+ if (files.length === 0) {
+ toast.error(t("addDetailDialog.selectFilesError"));
+ return;
+ }
+ }
+
+ // Edit 모드일 때는 detailDrawing 필수
+ if (mode === "edit" && !detailDrawing) {
+ toast.error(t("editDetailDialog.editError"));
return;
}
try {
setIsSubmitting(true);
- // 파일 업로드 ID 생성
- const uploadId = uuidv4();
-
- // 상세도면 추가
- const result = await editDetailDwgReceipt({
- dwgList: [
- {
- Mode: "ADD",
- Status: "Draft",
- RegisterId: 0,
- ProjectNo: drawing.ProjectNo,
- Discipline: drawing.Discipline,
- DrawingKind: drawing.DrawingKind,
- DrawingNo: drawing.DrawingNo,
- DrawingName: drawing.DrawingName,
- RegisterGroupId: drawing.RegisterGroupId,
- RegisterSerialNo: 0, // 자동 증가
- RegisterKind: registerKind,
- DrawingRevNo: revision,
- Category: "TS", // To SHI (벤더가 SHI에게 제출)
- Receiver: null,
- Manager: "",
- RegisterDesc: "",
- UploadId: uploadId,
- RegCompanyCode: vendorCode,
- },
- ],
- userId,
- userNm: userName,
- vendorCode,
- email: userEmail,
- });
-
- if (result > 0) {
- // 파일 업로드 처리 (상세도면 추가 후)
- if (files.length > 0) {
- toast.info(`${files.length}개 파일 업로드를 진행합니다...`);
-
- // 모든 파일 상태를 uploading으로 변경
- files.forEach((_, index) => {
- updateFileProgress(index, 0, "uploading");
- });
-
- const uploadResult = await uploadFilesWithProgress({
- uploadId,
- userId,
- files,
- callbacks: {
- onProgress: (fileIndex, progress) => {
- updateFileProgress(fileIndex, progress, "uploading");
- },
- onFileComplete: (fileIndex) => {
- updateFileProgress(fileIndex, 100, "completed");
- },
- onFileError: (fileIndex, error) => {
- updateFileProgress(fileIndex, 0, "error", error);
- },
+ if (mode === "add" && drawing) {
+ // 파일 업로드 ID 생성
+ const uploadId = uuidv4();
+
+ // 상세도면 추가
+ const result = await editDetailDwgReceipt({
+ dwgList: [
+ {
+ Mode: "ADD",
+ Status: "Submitted",
+ RegisterId: 0,
+ ProjectNo: drawing.ProjectNo,
+ Discipline: drawing.Discipline,
+ DrawingKind: drawing.DrawingKind,
+ DrawingNo: drawing.DrawingNo,
+ DrawingName: drawing.DrawingName,
+ RegisterGroupId: drawing.RegisterGroupId,
+ RegisterSerialNo: 0, // 자동 증가
+ RegisterKind: registerKind,
+ DrawingRevNo: revision,
+ Category: "TS", // To SHI (벤더가 SHI에게 제출)
+ Receiver: null,
+ Manager: "",
+ RegisterDesc: comment,
+ UploadId: uploadId,
+ RegCompanyCode: vendorCode,
},
- });
-
- if (uploadResult.success) {
- toast.success(`상세도면 추가 및 ${uploadResult.uploadedCount}개 파일 업로드 완료`);
+ ],
+ userId,
+ userNm: userName,
+ vendorCode,
+ email: userEmail,
+ });
+
+ if (result > 0) {
+ // 파일 업로드 처리 (상세도면 추가 후)
+ if (files.length > 0) {
+ toast.info(t("addDetailDialog.uploadingFiles", { count: files.length }));
+
+ // 모든 파일 상태를 uploading으로 변경
+ files.forEach((_, index) => {
+ updateFileProgress(index, 0, "uploading");
+ });
+
+ const uploadResult = await uploadFilesWithProgress({
+ uploadId,
+ userId,
+ files,
+ callbacks: {
+ onProgress: (fileIndex, progress) => {
+ updateFileProgress(fileIndex, progress, "uploading");
+ },
+ onFileComplete: (fileIndex) => {
+ updateFileProgress(fileIndex, 100, "completed");
+ },
+ onFileError: (fileIndex, error) => {
+ updateFileProgress(fileIndex, 0, "error", error);
+ },
+ },
+ });
+
+ if (uploadResult.success) {
+ toast.success(t("addDetailDialog.addSuccessWithUpload", { count: uploadResult.uploadedCount }));
+ } else {
+ toast.warning(t("addDetailDialog.addSuccessPartialUpload", { error: uploadResult.error }));
+ }
} else {
- toast.warning(`상세도면은 추가되었으나 파일 업로드 실패: ${uploadResult.error}`);
+ toast.success(t("addDetailDialog.addSuccess"));
}
+
+ // API 호출 성공 시 무조건 다이얼로그 닫기 (파일 업로드 성공 여부와 무관)
+ resetForm();
+ onComplete();
+ onOpenChange(false);
} else {
- toast.success("상세도면이 추가되었습니다");
+ toast.error(t("addDetailDialog.addError"));
+ }
+ } else if (mode === "edit" && detailDrawing) {
+ // 상세도면 수정
+ const result = await editDetailDwgReceipt({
+ dwgList: [
+ {
+ Mode: "MOD",
+ Status: detailDrawing.Status,
+ RegisterId: detailDrawing.RegisterId,
+ ProjectNo: detailDrawing.ProjectNo,
+ Discipline: detailDrawing.Discipline,
+ DrawingKind: detailDrawing.DrawingKind,
+ DrawingNo: detailDrawing.DrawingNo,
+ DrawingName: detailDrawing.DrawingName,
+ RegisterGroupId: detailDrawing.RegisterGroupId,
+ RegisterSerialNo: detailDrawing.RegisterSerialNo,
+ RegisterKind: registerKind,
+ DrawingRevNo: revision,
+ Category: detailDrawing.Category,
+ Receiver: detailDrawing.Receiver,
+ Manager: detailDrawing.Manager,
+ RegisterDesc: comment,
+ UploadId: detailDrawing.UploadId,
+ RegCompanyCode: detailDrawing.RegCompanyCode || vendorCode,
+ },
+ ],
+ userId,
+ userNm: userName,
+ vendorCode,
+ email: userEmail,
+ });
+
+ if (result > 0) {
+ toast.success(t("editDetailDialog.editSuccess"));
+ resetForm();
+ onComplete();
+ onOpenChange(false);
+ } else {
+ toast.error(t("editDetailDialog.editError"));
}
-
- // API 호출 성공 시 무조건 다이얼로그 닫기 (파일 업로드 성공 여부와 무관)
- resetForm();
- onComplete();
- onOpenChange(false);
- } else {
- toast.error("상세도면 추가에 실패했습니다");
}
} catch (error) {
- console.error("상세도면 추가 실패:", error);
- toast.error("상세도면 추가 중 오류가 발생했습니다");
+ console.error("상세도면 처리 실패:", error);
+ toast.error(mode === "add" ? t("addDetailDialog.addErrorMessage") : t("editDetailDialog.editErrorMessage"));
} finally {
setIsSubmitting(false);
}
@@ -270,24 +341,29 @@ export function AddDetailDrawingDialog({
// 선택된 RegisterKind의 Revision Rule
const revisionRule = registerKindOptions.find((opt) => opt.value === registerKind)?.revisionRule || "";
- // 추가 버튼 활성화 조건
- const isFormValid =
- drawingUsage.trim() !== "" &&
- registerKind.trim() !== "" &&
- revision.trim() !== "" &&
- !revisionError &&
- files.length > 0;
+ // 버튼 활성화 조건
+ const isFormValid = mode === "add"
+ ? drawingUsage.trim() !== "" &&
+ registerKind.trim() !== "" &&
+ revision.trim() !== "" &&
+ !revisionError &&
+ files.length > 0
+ : registerKind.trim() !== "" &&
+ revision.trim() !== "" &&
+ !revisionError;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
- <DialogTitle>상세도면 추가</DialogTitle>
+ <DialogTitle>
+ {mode === "edit" ? t("editDetailDialog.title") : t("addDetailDialog.title")}
+ </DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* 도면 정보 표시 */}
- {drawing && (
+ {mode === "add" && drawing && (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
@@ -297,33 +373,45 @@ export function AddDetailDrawingDialog({
</Alert>
)}
- {/* 도면용도 선택 */}
- <div className="space-y-2">
- <Label>도면용도 (Drawing Usage)</Label>
- <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}>
- <SelectTrigger>
- <SelectValue placeholder="도면용도를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {drawingUsageOptions.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
+ {mode === "edit" && detailDrawing && (
+ <Alert>
+ <Info className="h-4 w-4" />
+ <AlertDescription>
+ <div className="font-medium">{detailDrawing.DrawingNo} - Rev. {detailDrawing.DrawingRevNo}</div>
+ <div className="text-sm text-muted-foreground">{detailDrawing.DrawingName}</div>
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* 도면용도 선택 (Add 모드에서만 표시) */}
+ {mode === "add" && (
+ <div className="space-y-2">
+ <Label>{t("addDetailDialog.drawingUsageLabel")}</Label>
+ <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}>
+ <SelectTrigger>
+ <SelectValue placeholder={t("addDetailDialog.drawingUsagePlaceholder")} />
+ </SelectTrigger>
+ <SelectContent>
+ {drawingUsageOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
{/* 등록종류 선택 */}
<div className="space-y-2">
- <Label>등록종류 (Register Kind)</Label>
+ <Label>{t("addDetailDialog.registerKindLabel")}</Label>
<Select
value={registerKind}
onValueChange={setRegisterKind}
- disabled={!drawingUsage}
+ disabled={mode === "add" && !drawingUsage}
>
<SelectTrigger>
- <SelectValue placeholder="등록종류를 선택하세요" />
+ <SelectValue placeholder={t("addDetailDialog.registerKindPlaceholder")} />
</SelectTrigger>
<SelectContent>
{registerKindOptions.map((option) => (
@@ -335,18 +423,18 @@ export function AddDetailDrawingDialog({
</Select>
{revisionRule && (
<p className="text-sm text-muted-foreground">
- Revision 입력 형식: {revisionRule}
+ {t("addDetailDialog.revisionFormatPrefix")}{revisionRule}
</p>
)}
</div>
{/* Revision 입력 */}
<div className="space-y-2">
- <Label>Revision</Label>
+ <Label>{t("addDetailDialog.revisionLabel")}</Label>
<Input
value={revision}
onChange={(e) => handleRevisionChange(e.target.value)}
- placeholder="예: A, B, R00, R01"
+ placeholder={t("addDetailDialog.revisionPlaceholder")}
disabled={!registerKind}
className={revisionError ? "border-red-500 focus-visible:ring-red-500" : ""}
/>
@@ -357,14 +445,30 @@ export function AddDetailDrawingDialog({
)}
{!revisionError && revision && (
<p className="text-sm text-green-600 flex items-center gap-1">
- ✓ 올바른 형식입니다
+ {t("addDetailDialog.revisionValid")}
</p>
)}
</div>
- {/* 파일 업로드 */}
+ {/* Comment 입력 */}
<div className="space-y-2">
- <Label>첨부파일 (필수) *</Label>
+ <Label>{t("addDetailDialog.commentLabel")}</Label>
+ <Textarea
+ value={comment}
+ onChange={(e) => setComment(e.target.value)}
+ placeholder={t("addDetailDialog.commentPlaceholder")}
+ rows={3}
+ className="resize-none"
+ />
+ <p className="text-xs text-muted-foreground">
+ {t("addDetailDialog.commentMaxLength")}
+ </p>
+ </div>
+
+ {/* 파일 업로드 (Add 모드에서만 표시) */}
+ {mode === "add" && (
+ <div className="space-y-2">
+ <Label>{t("addDetailDialog.attachmentLabel")}</Label>
<div
{...getRootProps()}
className={`
@@ -380,20 +484,20 @@ export function AddDetailDrawingDialog({
<Upload className="h-8 w-8 mx-auto text-muted-foreground" />
<div>
<p className="text-sm font-medium">
- 파일을 드래그하거나 클릭하여 선택
+ {t("addDetailDialog.dragDropText")}
</p>
<p className="text-xs text-muted-foreground">
- 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일)
+ {t("addDetailDialog.fileInfo")}
</p>
</div>
</div>
) : (
<div className="space-y-2">
<p className="text-sm font-medium">
- {files.length}개 파일 선택됨
+ {t("addDetailDialog.filesSelected", { count: files.length })}
</p>
<p className="text-xs text-muted-foreground">
- 추가로 파일을 드래그하거나 클릭하여 더 추가할 수 있습니다
+ {t("addDetailDialog.addMoreFiles")}
</p>
</div>
)}
@@ -410,14 +514,14 @@ export function AddDetailDrawingDialog({
<>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium">
- 선택된 파일 ({files.length}개)
+ {t("addDetailDialog.selectedFiles", { count: files.length })}
</h4>
<Button
variant="ghost"
size="sm"
onClick={clearFiles}
>
- 전체 제거
+ {t("addDetailDialog.removeAll")}
</Button>
</div>
<div className="max-h-60 overflow-y-auto space-y-2">
@@ -447,15 +551,21 @@ export function AddDetailDrawingDialog({
)}
</div>
)}
- </div>
+ </div>
+ )}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
- 취소
+ {t("addDetailDialog.cancelButton")}
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting || !isFormValid}>
- {isSubmitting ? "처리 중..." : "추가"}
+ {isSubmitting
+ ? t("addDetailDialog.processingButton")
+ : mode === "edit"
+ ? t("editDetailDialog.updateButton")
+ : t("addDetailDialog.addButton")
+ }
</Button>
</DialogFooter>
</DialogContent>
@@ -463,3 +573,4 @@ export function AddDetailDrawingDialog({
);
}
+
diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx
index 3207c00b..ba5673ef 100644
--- a/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx
+++ b/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx
@@ -30,10 +30,13 @@ import {
} from "./b4-upload-validation-dialog";
import {
fetchDwgReceiptList,
- bulkUploadB4FilesV2,
+ prepareB4DetailDrawingsV2,
type B4BulkUploadResult,
type GttDwgReceiptItem,
} from "../actions";
+import { uploadFilesWithProgress } from "../utils/upload-with-progress";
+import { FileUploadProgressList } from "../components/file-upload-progress-list";
+import type { FileUploadProgress } from "../hooks/use-file-upload-with-progress";
interface B4BulkUploadDialogV2Props {
open: boolean;
@@ -71,6 +74,7 @@ export function B4BulkUploadDialogV2({
const [isDragging, setIsDragging] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null);
+ const [fileProgresses, setFileProgresses] = useState<FileUploadProgress[]>([]);
// B4 GTT 옵션
const drawingUsageOptions = [
@@ -96,6 +100,7 @@ export function B4BulkUploadDialogV2({
setIsDragging(false);
setUploadProgress(0);
setUploadResult(null);
+ setFileProgresses([]);
}
}, [open]);
@@ -271,7 +276,7 @@ export function B4BulkUploadDialogV2({
}
};
- // 업로드 확인 (V2: bulkUploadB4FilesV2 사용)
+ // 업로드 확인 (V2: prepareB4DetailDrawingsV2 + uploadFilesWithProgress 사용)
const handleConfirmUpload = async (validFiles: FileValidationResult[]) => {
setIsUploading(true);
setCurrentStep("uploading");
@@ -280,35 +285,224 @@ export function B4BulkUploadDialogV2({
try {
console.log(`[V2 Dialog] 업로드 시작: ${validFiles.length}개 파일`);
- // FormData 구성
- const formData = new FormData();
- formData.append("projectNo", projectNo);
- formData.append("userId", userId);
- formData.append("userNm", userName);
- formData.append("email", userEmail);
- formData.append("vendorCode", vendorCode);
- formData.append("registerKind", registerKind);
- formData.append("fileCount", String(validFiles.length));
+ // 0단계: 모든 파일에 대한 진행도 상태 초기화
+ const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({
+ file: fileResult.file,
+ progress: 0,
+ status: "pending" as const,
+ }));
+ setFileProgresses(initialProgresses);
+ // 파일 인덱스 맵 생성 (파일명 기반)
+ const fileIndexMap = new Map<string, number>();
validFiles.forEach((fileResult, index) => {
- formData.append(`file_${index}`, fileResult.file);
+ fileIndexMap.set(fileResult.file.name, index);
});
- // 업로드 프로그레스 시뮬레이션
- const progressInterval = setInterval(() => {
- setUploadProgress((prev) => {
- if (prev >= 90) return 90;
- return prev + 10;
- });
- }, 500);
+ // 1단계: DrawingNo + RevNo별로 그룹화
+ // - 동일한 Drawing/Revision에 속하는 파일들을 하나의 그룹으로 묶음
+ // - 이렇게 하면 같은 리비전의 상세도면을 1번만 생성/조회함
+ const uploadGroups = new Map<
+ string,
+ {
+ drawingNo: string;
+ revNo: string;
+ files: File[];
+ fileIndices: number[]; // 전체 배열에서의 인덱스
+ }
+ >();
+
+ validFiles.forEach((fileResult, index) => {
+ const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`;
+ if (!uploadGroups.has(groupKey)) {
+ uploadGroups.set(groupKey, {
+ drawingNo: fileResult.parsed!.drawingNo,
+ revNo: fileResult.parsed!.revNo,
+ files: [],
+ fileIndices: [],
+ });
+ }
+ uploadGroups.get(groupKey)!.files.push(fileResult.file);
+ uploadGroups.get(groupKey)!.fileIndices.push(index);
+ });
+
+ console.log(
+ `[V2 Dialog] ${uploadGroups.size}개 리비전 그룹 생성 (${validFiles.length}개 파일)`
+ );
+
+ // 2단계: 상세도면 준비 (서버 액션)
+ // - 각 리비전별로 상세도면 존재 여부 확인
+ // - 기존 상세도면이 있으면 uploadId 재사용
+ // - 없으면 새로 생성 (1번만!)
+ const drawingRevisions = Array.from(uploadGroups.values()).map((group) => ({
+ drawingNo: group.drawingNo,
+ revNo: group.revNo,
+ }));
+
+ console.log(`[V2 Dialog] 상세도면 준비 요청: ${drawingRevisions.length}개 리비전`);
+
+ const prepareResult = await prepareB4DetailDrawingsV2({
+ projectNo,
+ userId,
+ userNm: userName,
+ email: userEmail,
+ vendorCode,
+ registerKind,
+ drawingRevisions,
+ });
+
+ if (!prepareResult.success || !prepareResult.detailDrawings) {
+ throw new Error(prepareResult.error || "상세도면 준비 실패");
+ }
+
+ const newDrawings = prepareResult.detailDrawings.filter((d) => d.isNew);
+ const existingDrawings = prepareResult.detailDrawings.filter((d) => !d.isNew);
+
+ console.log(
+ `[V2 Dialog] 상세도면 준비 완료: 총 ${prepareResult.detailDrawings.length}개 ` +
+ `(기존 ${existingDrawings.length}개 재사용, 신규 ${newDrawings.length}개 생성)`
+ );
+
+ // 3단계: 각 그룹별로 파일 업로드
+ // - 준비된 uploadId를 사용하여 파일 업로드
+ const detailDrawingMap = new Map(
+ prepareResult.detailDrawings.map((d) => [`${d.drawingNo}_${d.revNo}`, d])
+ );
+
+ let successCount = 0;
+ let failCount = 0;
+ let completedGroups = 0;
+ const results: B4BulkUploadResult["results"] = [];
+
+ for (const [groupKey, group] of uploadGroups.entries()) {
+ try {
+ const detailDrawing = detailDrawingMap.get(groupKey);
+ if (!detailDrawing) {
+ throw new Error(`상세도면 정보를 찾을 수 없습니다: ${groupKey}`);
+ }
- // V2 함수 호출
- const result = await bulkUploadB4FilesV2(formData);
+ console.log(
+ `[V2 Dialog] 그룹 ${groupKey} 업로드 시작\n` +
+ ` - 파일 수: ${group.files.length}개\n` +
+ ` - UploadId: ${detailDrawing.uploadId}\n` +
+ ` - 상태: ${detailDrawing.isNew ? "신규 생성" : "기존 재사용"}`
+ );
+
+ // 그룹 내 모든 파일 상태를 uploading으로 변경
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ group.fileIndices.includes(index)
+ ? { ...fp, status: "uploading" as const }
+ : fp
+ )
+ );
+
+ // uploadFilesWithProgress 사용 (클라이언트 fetch)
+ const uploadResult = await uploadFilesWithProgress({
+ uploadId: detailDrawing.uploadId,
+ userId: userId,
+ files: group.files,
+ callbacks: {
+ onProgress: (fileIndexInGroup, progress) => {
+ // 그룹 내 파일 인덱스를 전체 인덱스로 변환
+ const globalFileIndex = group.fileIndices[fileIndexInGroup];
+
+ // 개별 파일 진행도 업데이트
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ index === globalFileIndex
+ ? { ...fp, progress, status: "uploading" as const }
+ : fp
+ )
+ );
+
+ // 전체 진행도 계산
+ const groupProgress = (completedGroups / uploadGroups.size) * 100;
+ const currentGroupProgress = (progress / 100) * (100 / uploadGroups.size);
+ setUploadProgress(Math.round(groupProgress + currentGroupProgress));
+ },
+ onFileComplete: (fileIndexInGroup) => {
+ const globalFileIndex = group.fileIndices[fileIndexInGroup];
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ index === globalFileIndex
+ ? { ...fp, progress: 100, status: "completed" as const }
+ : fp
+ )
+ );
+ },
+ onFileError: (fileIndexInGroup, error) => {
+ const globalFileIndex = group.fileIndices[fileIndexInGroup];
+ console.error(`[V2 Dialog] 파일 ${globalFileIndex} 업로드 실패:`, error);
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ index === globalFileIndex
+ ? { ...fp, status: "error" as const, error }
+ : fp
+ )
+ );
+ },
+ },
+ });
- clearInterval(progressInterval);
- setUploadProgress(100);
+ if (uploadResult.success) {
+ console.log(
+ `[V2 Dialog] ✓ 그룹 ${groupKey} 업로드 완료 (${group.files.length}개 파일)`
+ );
+ successCount += group.files.length;
+
+ // 성공 결과 추가
+ group.files.forEach((file) => {
+ results?.push({
+ drawingNo: group.drawingNo,
+ revNo: group.revNo,
+ fileName: file.name,
+ success: true,
+ });
+ });
+ } else {
+ throw new Error(uploadResult.error || "파일 업로드 실패");
+ }
+ } catch (error) {
+ console.error(
+ `[V2 Dialog] ✗ 그룹 ${groupKey} 업로드 실패 (${group.files.length}개 파일):`,
+ error
+ );
+ const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
+ failCount += group.files.length;
+
+ // 실패 결과 추가
+ group.files.forEach((file) => {
+ results?.push({
+ drawingNo: group.drawingNo,
+ revNo: group.revNo,
+ fileName: file.name,
+ success: false,
+ error: errorMessage,
+ });
+ });
+ }
- console.log("[V2 Dialog] 업로드 완료:", result);
+ completedGroups++;
+ setUploadProgress(Math.round((completedGroups / uploadGroups.size) * 100));
+ }
+
+ console.log(
+ `[V2 Dialog] ========================================\n` +
+ `[V2 Dialog] 일괄 업로드 최종 결과\n` +
+ `[V2 Dialog] - 총 파일 수: ${validFiles.length}개\n` +
+ `[V2 Dialog] - 성공: ${successCount}개\n` +
+ `[V2 Dialog] - 실패: ${failCount}개\n` +
+ `[V2 Dialog] - 처리된 리비전: ${uploadGroups.size}개\n` +
+ `[V2 Dialog] ========================================`
+ );
+
+ const result: B4BulkUploadResult = {
+ success: successCount > 0,
+ successCount,
+ failCount,
+ results,
+ };
setUploadResult(result);
setCurrentStep("complete");
@@ -506,7 +700,7 @@ export function B4BulkUploadDialogV2({
{/* 4단계: 업로드 진행 중 */}
{currentStep === "uploading" && (
- <div className="space-y-6 py-8">
+ <div className="space-y-6 py-4">
<div className="flex flex-col items-center">
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
<h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploading")}</h3>
@@ -514,18 +708,22 @@ export function B4BulkUploadDialogV2({
{t("bulkUpload.uploadingWait")}
</p>
</div>
+
+ {/* 전체 진행도 */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>{t("bulkUpload.uploadProgress")}</span>
<span>{uploadProgress}%</span>
</div>
<Progress value={uploadProgress} className="h-2" />
- {uploadProgress >= 90 && uploadProgress < 100 && (
- <p className="text-xs text-muted-foreground text-center pt-2">
- {t("bulkUpload.uploadingToServer")}
- </p>
- )}
</div>
+
+ {/* 개별 파일 진행도 리스트 */}
+ {fileProgresses.length > 0 && (
+ <div className="border rounded-lg p-4 max-h-96 overflow-y-auto">
+ <FileUploadProgressList fileProgresses={fileProgresses} />
+ </div>
+ )}
</div>
)}
diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
index 21647e63..b7b25fca 100644
--- a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
+++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
@@ -36,6 +36,9 @@ import {
type B4MappingSaveItem,
} from "../actions";
import { v4 as uuidv4 } from "uuid";
+import { uploadFilesWithProgress } from "../utils/upload-with-progress";
+import { FileUploadProgressList } from "../components/file-upload-progress-list";
+import type { FileUploadProgress } from "../hooks/use-file-upload-with-progress";
interface B4BulkUploadDialogProps {
open: boolean;
@@ -73,6 +76,7 @@ export function B4BulkUploadDialog({
const [isDragging, setIsDragging] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null);
+ const [fileProgresses, setFileProgresses] = useState<FileUploadProgress[]>([]);
// B4 GTT 옵션 (코드 번역 유틸리티 사용)
const drawingUsageOptions = [
@@ -98,6 +102,7 @@ export function B4BulkUploadDialog({
setIsDragging(false);
setUploadProgress(0);
setUploadResult(null);
+ setFileProgresses([]);
}
}, [open]);
@@ -275,6 +280,14 @@ export function B4BulkUploadDialog({
try {
console.log(`[B4 일괄 업로드] 시작: ${validFiles.length}개 파일`);
+ // 0단계: 모든 파일에 대한 진행도 상태 초기화
+ const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({
+ file: fileResult.file,
+ progress: 0,
+ status: "pending" as const,
+ }));
+ setFileProgresses(initialProgresses);
+
// 파일을 DrawingNo + RevNo로 그룹화
const uploadGroups = new Map<
string,
@@ -284,10 +297,11 @@ export function B4BulkUploadDialog({
revNo: string;
fileName: string;
registerGroupId: number;
+ fileIndex: number; // 전체 배열에서의 인덱스
}>
>();
- validFiles.forEach((fileResult) => {
+ validFiles.forEach((fileResult, index) => {
const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`;
if (!uploadGroups.has(groupKey)) {
uploadGroups.set(groupKey, []);
@@ -298,6 +312,7 @@ export function B4BulkUploadDialog({
revNo: fileResult.parsed!.revNo,
fileName: fileResult.file.name,
registerGroupId: fileResult.registerGroupId || 0,
+ fileIndex: index,
});
});
@@ -317,27 +332,63 @@ export function B4BulkUploadDialog({
// 1. UploadId 생성
const uploadId = uuidv4();
- // 2. 파일 업로드 (공통 API 사용)
- const formData = new FormData();
- formData.append("uploadId", uploadId);
- formData.append("userId", userId);
- formData.append("fileCount", String(files.length));
-
- files.forEach((fileInfo, index) => {
- formData.append(`file_${index}`, fileInfo.file);
- });
-
- const uploadResponse = await fetch("/api/dolce/upload-files", {
- method: "POST",
- body: formData,
+ // 그룹 내 모든 파일 상태를 uploading으로 변경
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ files.some((f) => f.fileIndex === index)
+ ? { ...fp, status: "uploading" as const }
+ : fp
+ )
+ );
+
+ // 2. 파일 업로드 (uploadFilesWithProgress 사용)
+ const uploadResult = await uploadFilesWithProgress({
+ uploadId,
+ userId,
+ files: files.map((f) => f.file),
+ callbacks: {
+ onProgress: (fileIndexInGroup, progress) => {
+ // 그룹 내 파일 인덱스를 전체 인덱스로 변환
+ const globalFileIndex = files[fileIndexInGroup].fileIndex;
+
+ // 개별 파일 진행도 업데이트
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ index === globalFileIndex
+ ? { ...fp, progress, status: "uploading" as const }
+ : fp
+ )
+ );
+
+ // 전체 진행도 계산
+ const groupProgress = (completedGroups / uploadGroups.size) * 100;
+ const currentGroupProgress = (progress / 100) * (100 / uploadGroups.size);
+ setUploadProgress(Math.round(groupProgress + currentGroupProgress));
+ },
+ onFileComplete: (fileIndexInGroup) => {
+ const globalFileIndex = files[fileIndexInGroup].fileIndex;
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ index === globalFileIndex
+ ? { ...fp, progress: 100, status: "completed" as const }
+ : fp
+ )
+ );
+ },
+ onFileError: (fileIndexInGroup, error) => {
+ const globalFileIndex = files[fileIndexInGroup].fileIndex;
+ console.error(`[B4 업로드] 파일 ${globalFileIndex} 업로드 실패:`, error);
+ setFileProgresses((prev) =>
+ prev.map((fp, index) =>
+ index === globalFileIndex
+ ? { ...fp, status: "error" as const, error }
+ : fp
+ )
+ );
+ },
+ },
});
- if (!uploadResponse.ok) {
- throw new Error(`파일 업로드 실패: ${uploadResponse.status}`);
- }
-
- const uploadResult = await uploadResponse.json();
-
if (!uploadResult.success) {
throw new Error(uploadResult.error || "파일 업로드 실패");
}
@@ -599,7 +650,7 @@ export function B4BulkUploadDialog({
{/* 4단계: 업로드 진행 중 */}
{currentStep === "uploading" && (
- <div className="space-y-6 py-8">
+ <div className="space-y-6 py-4">
<div className="flex flex-col items-center">
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
<h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploading")}</h3>
@@ -607,19 +658,22 @@ export function B4BulkUploadDialog({
{t("bulkUpload.uploadingWait")}
</p>
</div>
+
+ {/* 전체 진행도 */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>{t("bulkUpload.uploadProgress")}</span>
<span>{uploadProgress}%</span>
</div>
<Progress value={uploadProgress} className="h-2" />
- {/* 90% 이상일 때 추가 안내 메시지 */}
- {uploadProgress >= 90 && uploadProgress < 100 && (
- <p className="text-xs text-muted-foreground text-center pt-2">
- {t("bulkUpload.uploadingToServer")}
- </p>
- )}
</div>
+
+ {/* 개별 파일 진행도 리스트 */}
+ {fileProgresses.length > 0 && (
+ <div className="border rounded-lg p-4 max-h-96 overflow-y-auto">
+ <FileUploadProgressList fileProgresses={fileProgresses} />
+ </div>
+ )}
</div>
)}
diff --git a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
index f3a7c70a..05c1efd7 100644
--- a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
+++ b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
@@ -38,12 +38,12 @@ interface B4UploadValidationDialogProps {
}
/**
- * B4 파일명 검증 함수
- * 형식: [버림] [문서번호토큰1] [문서번호토큰2] ... [리비전번호].[확장자]
- * 예시: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01"
- * - 첫 번째 토큰은 버림
- * - 마지막 토큰은 RevNo
- * - 중간 토큰들을 "-"로 연결하여 DrawingNo 생성
+ * B4 file name validation function
+ * Format: [ignore] [document_number_token1] [document_number_token2] ... [revision_number].[extension]
+ * Example: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01"
+ * - First token is ignored
+ * - Last token is RevNo
+ * - Middle tokens are joined with "-" to create DrawingNo
*/
export function validateB4FileName(fileName: string): {
valid: boolean;
@@ -51,47 +51,47 @@ export function validateB4FileName(fileName: string): {
error?: string;
} {
try {
- // 확장자 분리
+ // Separate extension
const lastDotIndex = fileName.lastIndexOf(".");
if (lastDotIndex === -1) {
return {
valid: false,
- error: "파일 확장자가 없습니다",
+ error: "File has no extension",
};
}
const nameWithoutExt = fileName.substring(0, lastDotIndex);
- // 공백으로 분리
+ // Split by spaces
const parts = nameWithoutExt.split(" ").filter(p => p.trim() !== "");
- // 최소 3개 파트 필요: [버림], [문서번호토큰], [RevNo]
+ // At least 3 parts required: [ignore], [document_number_token], [RevNo]
if (parts.length < 3) {
return {
valid: false,
- error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [문서번호토큰들...] [RevNo].[확장자]`,
+ error: `At least 2 spaces required (current: ${parts.length - 1}). Format: [ignore] [document_tokens...] [RevNo].[extension]`,
};
}
- // 첫 번째 토큰은 버림
- // 마지막 토큰은 RevNo
- // 중간 토큰들을 "-"로 연결하여 DrawingNo 생성
+ // First token is ignored
+ // Last token is RevNo
+ // Middle tokens are joined with "-" to create DrawingNo
const revNo = parts[parts.length - 1];
const drawingTokens = parts.slice(1, parts.length - 1);
const drawingNo = drawingTokens.join("-");
- // 필수 항목이 비어있지 않은지 확인
+ // Check that required fields are not empty
if (!drawingNo || drawingNo.trim() === "") {
return {
valid: false,
- error: "도면번호(DrawingNo)가 비어있습니다",
+ error: "Drawing number (DrawingNo) is empty",
};
}
if (!revNo || revNo.trim() === "") {
return {
valid: false,
- error: "리비전 번호(RevNo)가 비어있습니다",
+ error: "Revision number (RevNo) is empty",
};
}
@@ -106,13 +106,13 @@ export function validateB4FileName(fileName: string): {
} catch (error) {
return {
valid: false,
- error: error instanceof Error ? error.message : "알 수 없는 오류",
+ error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
- * B4 업로드 전 파일 검증 다이얼로그
+ * B4 file validation dialog before upload
*/
export function B4UploadValidationDialog({
open,
@@ -141,45 +141,45 @@ export function B4UploadValidationDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
- <DialogTitle>B4 일괄 업로드 검증</DialogTitle>
+ <DialogTitle>B4 Bulk Upload Validation</DialogTitle>
<DialogDescription>
- 선택한 파일의 파일명 형식과 매핑 가능 여부를 검증합니다
+ Validates file name format and mapping availability for selected files
</DialogDescription>
</DialogHeader>
<div className="space-y-4 overflow-auto flex-1 pr-2">
- {/* 요약 통계 */}
+ {/* Summary statistics */}
<div className="grid grid-cols-4 gap-3">
<div className="rounded-lg border p-3">
- <div className="text-sm text-muted-foreground">전체</div>
+ <div className="text-sm text-muted-foreground">Total</div>
<div className="text-2xl font-bold">{validationResults.length}</div>
</div>
<div className="rounded-lg border p-3 bg-green-50 dark:bg-green-950/30">
- <div className="text-sm text-green-600 dark:text-green-400">업로드 가능</div>
+ <div className="text-sm text-green-600 dark:text-green-400">Ready to Upload</div>
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{validFiles.length}
</div>
</div>
<div className="rounded-lg border p-3 bg-orange-50 dark:bg-orange-950/30">
- <div className="text-sm text-orange-600 dark:text-orange-400">도면 없음</div>
+ <div className="text-sm text-orange-600 dark:text-orange-400">Drawing Not Found</div>
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">
{notFoundFiles.length}
</div>
</div>
<div className="rounded-lg border p-3 bg-red-50 dark:bg-red-950/30">
- <div className="text-sm text-red-600 dark:text-red-400">형식 오류</div>
+ <div className="text-sm text-red-600 dark:text-red-400">Format Error</div>
<div className="text-2xl font-bold text-red-600 dark:text-red-400">
{invalidFiles.length}
</div>
</div>
</div>
- {/* 경고 메시지 */}
+ {/* Warning messages */}
{validFiles.length === 0 && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription>
- 업로드 가능한 파일이 없습니다. 파일명 형식을 확인하거나 이미 매핑된 파일은 제외해주세요.
+ No files available for upload. Please check the file name format or exclude already mapped files.
</AlertDescription>
</Alert>
)}
@@ -188,20 +188,20 @@ export function B4UploadValidationDialog({
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
- 일부 파일에 문제가 있습니다. 업로드 가능한 {validFiles.length}개 파일만 업로드됩니다.
+ Some files have issues. Only {validFiles.length} file(s) will be uploaded.
</AlertDescription>
</Alert>
)}
- {/* 파일 목록 */}
+ {/* File list */}
<div className="max-h-[50vh] overflow-auto rounded-md border p-4">
<div className="space-y-4">
- {/* 업로드 가능 파일 */}
+ {/* Files ready to upload */}
{validFiles.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-600 dark:text-green-400 flex items-center gap-2">
<CheckCircle2 className="h-4 w-4" />
- 업로드 가능 ({validFiles.length}개)
+ Ready to Upload ({validFiles.length})
</h4>
{validFiles.map((result, index) => (
<div
@@ -216,7 +216,7 @@ export function B4UploadValidationDialog({
{result.parsed && (
<div className="flex flex-wrap gap-1 mt-2">
<Badge variant="outline" className="text-xs">
- 도면: {result.parsed.drawingNo}
+ Drawing: {result.parsed.drawingNo}
</Badge>
<Badge variant="outline" className="text-xs">
Rev: {result.parsed.revNo}
@@ -236,12 +236,12 @@ export function B4UploadValidationDialog({
</div>
)}
- {/* 도면을 찾을 수 없는 파일 */}
+ {/* Files with drawing not found */}
{notFoundFiles.length > 0 && (
<div className="space-y-2 mt-4">
<h4 className="text-sm font-semibold text-orange-600 dark:text-orange-400 flex items-center gap-2">
<XCircle className="h-4 w-4" />
- 도면을 찾을 수 없음 ({notFoundFiles.length}개)
+ Drawing Not Found ({notFoundFiles.length})
</h4>
{notFoundFiles.map((result, index) => (
<div
@@ -254,12 +254,12 @@ export function B4UploadValidationDialog({
{result.file.name}
</div>
<div className="text-xs text-orange-700 dark:text-orange-300 mt-1">
- ✗ 해당 도면번호가 프로젝트에 등록되어 있지 않습니다
+ ✗ This drawing number is not registered in the project
</div>
{result.parsed && (
<div className="flex flex-wrap gap-1 mt-2">
<Badge variant="outline" className="text-xs">
- 도면: {result.parsed.drawingNo}
+ Drawing: {result.parsed.drawingNo}
</Badge>
<Badge variant="outline" className="text-xs">
Rev: {result.parsed.revNo}
@@ -274,12 +274,12 @@ export function B4UploadValidationDialog({
</div>
)}
- {/* 형식 오류 파일 */}
+ {/* Files with format errors */}
{invalidFiles.length > 0 && (
<div className="space-y-2 mt-4">
<h4 className="text-sm font-semibold text-red-600 dark:text-red-400 flex items-center gap-2">
<XCircle className="h-4 w-4" />
- 파일명 형식 오류 ({invalidFiles.length}개)
+ File Name Format Error ({invalidFiles.length})
</h4>
{invalidFiles.map((result, index) => (
<div
@@ -306,25 +306,22 @@ export function B4UploadValidationDialog({
</div>
</div>
- {/* 형식 안내 */}
+ {/* Format guide */}
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-3">
<div className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">
- 📋 올바른 파일명 형식
+ 📋 Correct File Name Format
</div>
<code className="text-xs text-blue-700 dark:text-blue-300">
- [버림] [문서번호토큰1] [문서번호토큰2] ... [RevNo].[확장자]
+ [fileName(without blanks)] [document_token1] [document_token2] ... [RevNo].[extension]
</code>
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
- 예: testfile GTT DE 007 R01.pdf → DrawingNo: GTT-DE-007, Rev: R01
+ Example: testfile GTT DE 007 R01.pdf → DrawingNo: GTT-DE-007, Rev: R01
</div>
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
- ※ 첫 번째 단어는 무시됩니다
+ ※ The last word is the revision number (RevNo)
</div>
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
- ※ 마지막 단어는 리비전 번호(RevNo)입니다
- </div>
- <div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
- ※ 중간의 모든 단어는 &quot;-&quot;로 연결되어 문서번호(DrawingNo)가 됩니다
+ ※ All middle words are connected with &quot;-&quot; to become the document number (DrawingNo)
</div>
</div>
</div>
@@ -335,7 +332,7 @@ export function B4UploadValidationDialog({
onClick={handleCancel}
disabled={isUploading}
>
- 취소
+ Cancel
</Button>
<Button
onClick={handleUpload}
@@ -344,12 +341,12 @@ export function B4UploadValidationDialog({
{isUploading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
- 업로드 중...
+ Uploading...
</>
) : (
<>
<Upload className="h-4 w-4 mr-2" />
- 업로드 ({validFiles.length}개)
+ Upload ({validFiles.length})
</>
)}
</Button>
diff --git a/lib/dolce/dialogs/detail-drawing-dialog.tsx b/lib/dolce/dialogs/detail-drawing-dialog.tsx
index d9df58db..afe4a4c2 100644
--- a/lib/dolce/dialogs/detail-drawing-dialog.tsx
+++ b/lib/dolce/dialogs/detail-drawing-dialog.tsx
@@ -23,7 +23,7 @@ import {
import { DrawingListTable } from "../table/drawing-list-table";
import { createDetailDrawingColumns } from "../table/detail-drawing-columns";
import { createFileListColumns } from "../table/file-list-columns";
-import { AddDetailDrawingDialog } from "./add-detail-drawing-dialog";
+import { AddAndModifyDetailDrawingDialog } from "./add-and-modify-detail-drawing-dialog";
import { UploadFilesToDetailDialog } from "./upload-files-to-detail-dialog";
interface DetailDrawingDialogProps {
@@ -56,6 +56,8 @@ export function DetailDrawingDialog({
const [isLoading, setIsLoading] = useState(false);
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
const [addDialogOpen, setAddDialogOpen] = useState(false);
+ const [editDialogOpen, setEditDialogOpen] = useState(false);
+ const [editingDetailDrawing, setEditingDetailDrawing] = useState<DetailDwgReceiptItem | null>(null);
const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false);
// 상세도면 목록 로드
@@ -169,6 +171,17 @@ export function DetailDrawingDialog({
loadDetailDrawings();
};
+ const handleEditClick = (detailDrawing: DetailDwgReceiptItem) => {
+ setEditingDetailDrawing(detailDrawing);
+ setEditDialogOpen(true);
+ };
+
+ const handleEditComplete = () => {
+ setEditDialogOpen(false);
+ setEditingDetailDrawing(null);
+ loadDetailDrawings();
+ };
+
const handleUploadComplete = () => {
setUploadFilesDialogOpen(false);
loadFiles();
@@ -235,7 +248,7 @@ export function DetailDrawingDialog({
</CardHeader>
<CardContent className="flex-1 overflow-y-auto p-4">
<DrawingListTable<DetailDwgReceiptItem, unknown>
- columns={createDetailDrawingColumns(lng, t)}
+ columns={createDetailDrawingColumns(lng, t, handleEditClick)}
data={detailDrawings}
onRowClick={setSelectedDetail}
selectedRow={selectedDetail || undefined}
@@ -291,7 +304,7 @@ export function DetailDrawingDialog({
</DialogContent>
</Dialog>
- <AddDetailDrawingDialog
+ <AddAndModifyDetailDrawingDialog
open={addDialogOpen}
onOpenChange={setAddDialogOpen}
drawing={drawing}
@@ -302,6 +315,22 @@ export function DetailDrawingDialog({
onComplete={handleAddComplete}
drawingKind={drawingKind}
lng={lng}
+ mode="add"
+ />
+
+ <AddAndModifyDetailDrawingDialog
+ open={editDialogOpen}
+ onOpenChange={setEditDialogOpen}
+ drawing={drawing}
+ vendorCode={vendorCode}
+ userId={userId}
+ userName={userName}
+ userEmail={userEmail}
+ onComplete={handleEditComplete}
+ drawingKind={drawingKind}
+ lng={lng}
+ mode="edit"
+ detailDrawing={editingDetailDrawing}
/>
{selectedDetail && (
diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
index 09f68614..e8d82129 100644
--- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
+++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Upload, FolderOpen, Loader2, X, FileText, AlertCircle } from "lucide-react";
import { toast } from "sonner";
+import { useTranslation } from "@/i18n/client";
import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress";
import { uploadFilesWithProgress, type UploadResult } from "../utils/upload-with-progress";
import { FileUploadProgressList } from "../components/file-upload-progress-list";
@@ -26,7 +27,7 @@ interface UploadFilesToDetailDialogProps {
revNo: string;
userId: string;
onUploadComplete?: () => void;
- lng?: string; // i18n support
+ lng: string;
}
export function UploadFilesToDetailDialog({
@@ -37,7 +38,9 @@ export function UploadFilesToDetailDialog({
revNo,
userId,
onUploadComplete,
+ lng,
}: UploadFilesToDetailDialogProps) {
+ const { t } = useTranslation(lng, "dolce");
const [isUploading, setIsUploading] = useState(false);
// 파일 업로드 훅 사용 (진행도 추적)
@@ -62,7 +65,7 @@ export function UploadFilesToDetailDialog({
// 업로드 처리
const handleUpload = async () => {
if (selectedFiles.length === 0) {
- toast.error("파일을 선택해주세요");
+ toast.error(t("uploadFilesDialog.selectFilesError"));
return;
}
@@ -93,16 +96,16 @@ export function UploadFilesToDetailDialog({
});
if (result.success) {
- toast.success(`${result.uploadedCount}개 파일 업로드 완료`);
+ toast.success(t("uploadFilesDialog.uploadSuccess", { count: result.uploadedCount }));
onOpenChange(false);
onUploadComplete?.();
} else {
- toast.error(result.error || "업로드 실패");
+ toast.error(result.error || t("uploadFilesDialog.uploadError"));
}
} catch (error) {
console.error("업로드 실패:", error);
toast.error(
- error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다"
+ error instanceof Error ? error.message : t("uploadFilesDialog.uploadErrorMessage")
);
} finally {
setIsUploading(false);
@@ -113,9 +116,9 @@ export function UploadFilesToDetailDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
- <DialogTitle>파일 업로드</DialogTitle>
+ <DialogTitle>{t("uploadFilesDialog.title")}</DialogTitle>
<DialogDescription>
- {drawingNo} - Rev. {revNo}에 파일을 업로드합니다
+ {t("uploadFilesDialog.description", { drawingNo, revNo })}
</DialogDescription>
</DialogHeader>
@@ -124,7 +127,7 @@ export function UploadFilesToDetailDialog({
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
- 선택한 상세도면의 UploadId에 파일을 추가합니다. 파일 업로드 후 자동으로 메타데이터가 저장됩니다.
+ {t("uploadFilesDialog.alertMessage")}
</AlertDescription>
</Alert>
@@ -152,11 +155,11 @@ export function UploadFilesToDetailDialog({
}`}
>
{isDragActive
- ? "파일을 여기에 놓으세요"
- : "클릭하거나 파일을 드래그하여 선택"}
+ ? t("uploadFilesDialog.dropHereText")
+ : t("uploadFilesDialog.dragDropText")}
</p>
<p className="text-xs text-muted-foreground mt-1">
- 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일)
+ {t("uploadFilesDialog.fileInfo")}
</p>
</div>
</div>
@@ -172,14 +175,14 @@ export function UploadFilesToDetailDialog({
<>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium">
- 선택된 파일 ({selectedFiles.length}개)
+ {t("uploadFilesDialog.selectedFiles", { count: selectedFiles.length })}
</h4>
<Button
variant="ghost"
size="sm"
onClick={clearFiles}
>
- 전체 제거
+ {t("uploadFilesDialog.removeAll")}
</Button>
</div>
<div className="max-h-60 overflow-y-auto space-y-2">
@@ -219,7 +222,7 @@ export function UploadFilesToDetailDialog({
onClick={() => onOpenChange(false)}
disabled={isUploading}
>
- 취소
+ {t("uploadFilesDialog.cancelButton")}
</Button>
<Button
onClick={handleUpload}
@@ -228,12 +231,12 @@ export function UploadFilesToDetailDialog({
{isUploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
- 업로드 중...
+ {t("uploadFilesDialog.uploadingButton")}
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
- 업로드 ({selectedFiles.length}개)
+ {t("uploadFilesDialog.uploadButton", { count: selectedFiles.length })}
</>
)}
</Button>
diff --git a/lib/dolce/table/detail-drawing-columns.tsx b/lib/dolce/table/detail-drawing-columns.tsx
index c082333d..747173af 100644
--- a/lib/dolce/table/detail-drawing-columns.tsx
+++ b/lib/dolce/table/detail-drawing-columns.tsx
@@ -3,13 +3,25 @@
import { ColumnDef } from "@tanstack/react-table";
import { DetailDwgReceiptItem } from "../actions";
import { formatDolceDateTime } from "../utils/date-formatter";
+import { useState } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Edit } from "lucide-react";
// DOLCE API ENM 필드가 제대로 번역되지 않아 직접 매핑
const DRAWING_USAGE_MAP: Record<string, { ko: string; en: string }> = {
APP: { ko: "승인용", en: "Approval" },
WOR: { ko: "작업용", en: "Working" },
+ // 참조용은 eVCP에서 사용하지 않으나 추가해둠
+ REF: { ko: "참조용", en: "Reference" },
REC: { ko: "입수용", en: "GTT→SHI" },
SUB: { ko: "제출용", en: "SHI→GTT" },
+ CMT: { ko: "코멘트", en: "Comment" },
};
const REGISTER_KIND_MAP: Record<string, { ko: string; en: string }> = {
@@ -19,6 +31,19 @@ const REGISTER_KIND_MAP: Record<string, { ko: string; en: string }> = {
WORP: { ko: "작업용 입수도면(Partial)", en: "For Working(Partial)" },
RECW: { ko: "Working 도면입수(GTT→SHI)", en: "Working Dwg(GTT→SHI)" },
RECP: { ko: "Pre. 도면입수(GTT→SHI)", en: "Pre. Dwg(GTT→SHI)" },
+ GSUB: { ko: "SHI 입력 문서 (SHI→GTT)", en: "SHI Input Document(SHI→GTT)" },
+ CMTQ: { ko: "Comment-TQ", en: "Comment-TQ" },
+ CMTM: { ko: "MARK-UP", en: "MARK-UP" },
+ // FMEA는 미사용 코드
+ "FMEA-R1": { ko: "FMEA 1st Receipt", en: "FMEA 1st Receipt" },
+ "FMEA-R2": { ko: "FMEA 2nd Receipt", en: "FMEA 2nd Receipt" },
+ "FMEA-1": { ko: "FMEA 1st Submission", en: "FMEA 1st Submission" },
+ "FMEA-2": { ko: "FMEA 2nd Submission", en: "FMEA 2nd Submission" },
+ // 참조용 도면도 미사용 코드, 번역 받아야 적용 가능
+ PREF: { ko: "본선용 참조도면", en: "본선용 참조도면" },
+ RREF: { ko: "실적선 참조도면", en: "실적선 참조도면" },
+ "PREP-P": { ko: "본선용 참조도면(Partial)", en: "본선용 참조도면(Partial)" },
+ "RREP-P": { ko: "실적선 참조도면(Partial)", en: "실적선 참조도면(Partial)" },
};
// 카테고리는 API에서 ENM이 제공되는 것으로 가정 (필요시 추가)
@@ -36,11 +61,39 @@ const translateRegisterKind = (code: string | null, lng: string): string => {
return lng === "en" ? mapped.en : mapped.ko;
};
+// Comment Dialog Component
+function CommentDialog({ comment }: { comment: string }) {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+ <Button
+ variant="ghost"
+ className="h-auto py-1 px-2 text-left justify-start font-normal hover:bg-accent"
+ onClick={() => setOpen(true)}
+ >
+ <div className="truncate max-w-[200px]">{comment || "-"}</div>
+ </Button>
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent className="max-w-2xl">
+ <DialogHeader>
+ <DialogTitle>Comment</DialogTitle>
+ </DialogHeader>
+ <div className="max-h-96 overflow-y-auto">
+ <p className="whitespace-pre-wrap text-sm">{comment || "코멘트가 없습니다."}</p>
+ </div>
+ </DialogContent>
+ </Dialog>
+ </>
+ );
+}
+
// 다국어 지원 컬럼 생성 함수
export function createDetailDrawingColumns(
lng: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- t: any
+ t: any,
+ onEdit?: (detailDrawing: DetailDwgReceiptItem) => void
): ColumnDef<DetailDwgReceiptItem>[] {
return [
{
@@ -78,17 +131,18 @@ export function createDetailDrawingColumns(
return <div>{lng === "en" ? (categoryENM || categoryNM) : (categoryNM || categoryENM)}</div>;
},
},
- {
- accessorKey: "DrawingUsage",
- header: t("detailDrawing.columns.drawingUsage"),
- minSize: 100,
- cell: ({ row }) => {
- // API 응답의 DrawingUsage는 코드값이므로 직접 매핑하여 번역
- const usageCode = row.getValue("DrawingUsage") as string;
- const translated = translateDrawingUsage(usageCode, lng);
- return <div>{translated}</div>;
- },
- },
+ // RegisterKind 로 DrawingUsage를 알 수 있으므로 주석 처리
+ // {
+ // accessorKey: "DrawingUsage",
+ // header: t("detailDrawing.columns.drawingUsage"),
+ // minSize: 100,
+ // cell: ({ row }) => {
+ // // API 응답의 DrawingUsage는 코드값이므로 직접 매핑하여 번역
+ // const usageCode = row.getValue("DrawingUsage") as string;
+ // const translated = translateDrawingUsage(usageCode, lng);
+ // return <div>{translated}</div>;
+ // },
+ // },
{
accessorKey: "RegisterKind",
header: t("detailDrawing.columns.registerKind"),
@@ -119,5 +173,39 @@ export function createDetailDrawingColumns(
return <div className="text-sm text-muted-foreground">{formatDolceDateTime(date)}</div>;
},
},
+ {
+ accessorKey: "RegisterDesc",
+ header: "Comment",
+ minSize: 200,
+ maxSize: 200,
+ cell: ({ row }) => {
+ const comment = row.getValue("RegisterDesc") as string;
+ return <CommentDialog comment={comment} />;
+ },
+ },
+ {
+ id: "actions",
+ header: t("detailDrawing.columns.actions"),
+ minSize: 100,
+ maxSize: 100,
+ cell: ({ row }) => {
+ const status = row.getValue("Status") as string;
+ const isEditable = status === "Submitted";
+
+ return (
+ <div className="flex items-center justify-center">
+ <Button
+ variant="ghost"
+ size="sm"
+ disabled={!isEditable || !onEdit}
+ onClick={() => onEdit && onEdit(row.original)}
+ title={!isEditable ? t("editDetailDialog.statusSubmittedOnly") : t("editDetailDialog.editButton")}
+ >
+ <Edit className="h-4 w-4" />
+ </Button>
+ </div>
+ );
+ },
+ },
];
}