summaryrefslogtreecommitdiff
path: root/lib/dolce/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dolce/dialogs')
-rw-r--r--lib/dolce/dialogs/add-detail-drawing-dialog.tsx136
-rw-r--r--lib/dolce/dialogs/b4-bulk-upload-dialog.tsx283
-rw-r--r--lib/dolce/dialogs/b4-upload-validation-dialog.tsx30
-rw-r--r--lib/dolce/dialogs/detail-drawing-dialog.tsx53
-rw-r--r--lib/dolce/dialogs/upload-files-to-detail-dialog.tsx3
5 files changed, 334 insertions, 171 deletions
diff --git a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
index 34d06368..48614ecf 100644
--- a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
+++ b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
@@ -26,6 +26,12 @@ import { v4 as uuidv4 } from "uuid";
import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress";
import { uploadFilesWithProgress } from "../utils/upload-with-progress";
import { FileUploadProgressList } from "../components/file-upload-progress-list";
+import {
+ getB3DrawingUsageOptions,
+ getB3RegisterKindOptions,
+ getB4DrawingUsageOptions,
+ getB4RegisterKindOptions
+} from "../utils/code-translator";
interface AddDetailDrawingDialogProps {
open: boolean;
@@ -36,38 +42,10 @@ interface AddDetailDrawingDialogProps {
userName: string;
userEmail: string;
onComplete: () => void;
- drawingKind: "B3" | "B4"; // 추가
+ drawingKind: "B3" | "B4";
+ lng?: string; // i18n support
}
-// B3 벤더의 선택 옵션
-const B3_DRAWING_USAGE_OPTIONS = [
- { value: "APP", label: "APPROVAL (승인용)" },
- { value: "WOR", label: "WORKING (작업용)" },
-];
-
-const B3_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string; revisionRule: string }>> = {
- APP: [
- { value: "APPR", label: "승인용 도면 (Full)", revisionRule: "예: A, B, C 또는 R00, R01, R02" },
- { value: "APPR-P", label: "승인용 도면 (Partial)", revisionRule: "예: A, B, C 또는 R00, R01, R02" },
- ],
- WOR: [
- { value: "WORK", label: "작업용 입수도면 (Full)", revisionRule: "예: A, B, C 또는 R00, R01, R02" },
- { value: "WORK-P", label: "작업용 입수도면 (Partial)", revisionRule: "예: A, B, C 또는 R00, R01, R02" },
- ],
-};
-
-// B4 벤더(GTT)의 선택 옵션
-const B4_DRAWING_USAGE_OPTIONS = [
- { value: "REC", label: "RECEIVE (입수용)" },
-];
-
-const B4_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string; revisionRule: string }>> = {
- REC: [
- { value: "RECP", label: "Pre. 도면입수", revisionRule: "예: R00, R01, R02, R03" },
- { value: "RECW", label: "Working 도면입수", revisionRule: "예: R00, R01, R02, R03" },
- ],
-};
-
export function AddDetailDrawingDialog({
open,
onOpenChange,
@@ -78,12 +56,29 @@ export function AddDetailDrawingDialog({
userEmail,
onComplete,
drawingKind,
+ lng = "ko",
}: AddDetailDrawingDialogProps) {
const [drawingUsage, setDrawingUsage] = useState<string>("");
const [registerKind, setRegisterKind] = useState<string>("");
const [revision, setRevision] = useState<string>("");
+ const [revisionError, setRevisionError] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState(false);
+ // 옵션 생성 (다국어 지원)
+ const drawingUsageOptions = drawingKind === "B3"
+ ? getB3DrawingUsageOptions(lng)
+ : getB4DrawingUsageOptions(lng);
+
+ const registerKindOptions = drawingKind === "B3"
+ ? getB3RegisterKindOptions(drawingUsage, lng).map(opt => ({
+ ...opt,
+ revisionRule: lng === "ko" ? "예: A, B, C 또는 R00, R01, R02" : "e.g. A, B, C or R00, R01, R02"
+ }))
+ : getB4RegisterKindOptions(drawingUsage, lng).map(opt => ({
+ ...opt,
+ revisionRule: lng === "ko" ? "예: R00, R01, R02, R03" : "e.g. R00, R01, R02, R03"
+ }));
+
// 파일 업로드 훅 사용 (진행도 추적)
const {
fileProgresses,
@@ -96,11 +91,47 @@ export function AddDetailDrawingDialog({
isDragActive,
} = useFileUploadWithProgress();
+ // Revision 유효성 검증 함수
+ const validateRevision = (value: string): string => {
+ if (!value.trim()) {
+ return "Revision을 입력하세요";
+ }
+
+ const upperValue = value.toUpperCase().trim();
+
+ // A-Z 패턴 (단일 알파벳)
+ if (/^[A-Z]$/.test(upperValue)) {
+ return "";
+ }
+
+ // R00-R99 패턴
+ if (/^R\d{2}$/.test(upperValue)) {
+ return "";
+ }
+
+ return "올바른 형식이 아닙니다 (A-Z 또는 R00-R99)";
+ };
+
+ // Revision 입력 핸들러
+ const handleRevisionChange = (value: string) => {
+ const processedValue = value.toUpperCase();
+ setRevision(processedValue);
+
+ // 값이 있을 때만 validation
+ if (processedValue.trim()) {
+ const error = validateRevision(processedValue);
+ setRevisionError(error);
+ } else {
+ setRevisionError("");
+ }
+ };
+
// 폼 초기화
const resetForm = () => {
setDrawingUsage("");
setRegisterKind("");
setRevision("");
+ setRevisionError("");
clearFiles();
};
@@ -119,8 +150,18 @@ export function AddDetailDrawingDialog({
}
if (!revision.trim()) {
toast.error("Revision을 입력하세요");
+ setRevisionError("Revision을 입력하세요");
return;
}
+
+ // Revision 형식 검증
+ const revisionValidationError = validateRevision(revision);
+ if (revisionValidationError) {
+ toast.error(revisionValidationError);
+ setRevisionError(revisionValidationError);
+ return;
+ }
+
if (files.length === 0) {
toast.error("최소 1개 이상의 파일을 첨부해야 합니다");
return;
@@ -222,19 +263,21 @@ export function AddDetailDrawingDialog({
const handleDrawingUsageChange = (value: string) => {
setDrawingUsage(value);
setRegisterKind("");
+ setRevision("");
+ setRevisionError("");
};
- // 현재 선택 가능한 DrawingUsage 및 RegisterKind 옵션
- const drawingUsageOptions = drawingKind === "B4" ? B4_DRAWING_USAGE_OPTIONS : B3_DRAWING_USAGE_OPTIONS;
- const registerKindOptionsMap = drawingKind === "B4" ? B4_REGISTER_KIND_OPTIONS : B3_REGISTER_KIND_OPTIONS;
-
- const registerKindOptions = drawingUsage
- ? registerKindOptionsMap[drawingUsage] || []
- : [];
-
// 선택된 RegisterKind의 Revision Rule
const revisionRule = registerKindOptions.find((opt) => opt.value === registerKind)?.revisionRule || "";
+ // 추가 버튼 활성화 조건
+ const isFormValid =
+ drawingUsage.trim() !== "" &&
+ registerKind.trim() !== "" &&
+ revision.trim() !== "" &&
+ !revisionError &&
+ files.length > 0;
+
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
@@ -302,10 +345,21 @@ export function AddDetailDrawingDialog({
<Label>Revision</Label>
<Input
value={revision}
- onChange={(e) => setRevision(e.target.value)}
+ onChange={(e) => handleRevisionChange(e.target.value)}
placeholder="예: A, B, R00, R01"
disabled={!registerKind}
+ className={revisionError ? "border-red-500 focus-visible:ring-red-500" : ""}
/>
+ {revisionError && (
+ <p className="text-sm text-red-500 flex items-center gap-1">
+ {revisionError}
+ </p>
+ )}
+ {!revisionError && revision && (
+ <p className="text-sm text-green-600 flex items-center gap-1">
+ ✓ 올바른 형식입니다
+ </p>
+ )}
</div>
{/* 파일 업로드 */}
@@ -366,7 +420,7 @@ export function AddDetailDrawingDialog({
전체 제거
</Button>
</div>
- <div className="max-h-48 overflow-auto space-y-2">
+ <div className="max-h-60 overflow-y-auto space-y-2">
{files.map((file, index) => (
<div
key={index}
@@ -400,7 +454,7 @@ export function AddDetailDrawingDialog({
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
취소
</Button>
- <Button onClick={handleSubmit} disabled={isSubmitting}>
+ <Button onClick={handleSubmit} disabled={isSubmitting || !isFormValid}>
{isSubmitting ? "처리 중..." : "추가"}
</Button>
</DialogFooter>
diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
index f4816328..1be7f226 100644
--- a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
+++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
@@ -22,6 +22,7 @@ import {
import { FolderOpen, Loader2, ChevronRight, ChevronLeft, CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
import { Progress } from "@/components/ui/progress";
+import { useTranslation } from "@/i18n/client";
import {
validateB4FileName,
B4UploadValidationDialog,
@@ -29,10 +30,11 @@ import {
} from "./b4-upload-validation-dialog";
import {
checkB4MappingStatus,
- bulkUploadB4Files,
+ editDetailDwgReceipt,
type MappingCheckItem,
type B4BulkUploadResult,
} from "../actions";
+import { v4 as uuidv4 } from "uuid";
interface B4BulkUploadDialogProps {
open: boolean;
@@ -43,20 +45,9 @@ interface B4BulkUploadDialogProps {
userEmail: string;
vendorCode: string;
onUploadComplete?: () => void;
+ lng: string;
}
-// B4 GTT 옵션
-const B4_DRAWING_USAGE_OPTIONS = [
- { value: "REC", label: "RECEIVE (입수용)" },
-];
-
-const B4_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string }>> = {
- REC: [
- { value: "RECP", label: "Pre. 도면입수" },
- { value: "RECW", label: "Working 도면입수" },
- ],
-};
-
type UploadStep = "settings" | "files" | "validation" | "uploading" | "complete";
export function B4BulkUploadDialog({
@@ -68,7 +59,9 @@ export function B4BulkUploadDialog({
userEmail,
vendorCode,
onUploadComplete,
+ lng,
}: B4BulkUploadDialogProps) {
+ const { t } = useTranslation(lng, "dolce");
const [currentStep, setCurrentStep] = useState<UploadStep>("settings");
const [drawingUsage, setDrawingUsage] = useState<string>("REC");
const [registerKind, setRegisterKind] = useState<string>("");
@@ -80,6 +73,18 @@ export function B4BulkUploadDialog({
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null);
+ // B4 GTT 옵션 (코드 번역 유틸리티 사용)
+ const drawingUsageOptions = [
+ { value: "REC", label: t("bulkUpload.drawingUsageReceive") },
+ ];
+
+ const registerKindOptionsMap: Record<string, Array<{ value: string; label: string }>> = {
+ REC: [
+ { value: "RECP", label: t("bulkUpload.registerKindRecP") },
+ { value: "RECW", label: t("bulkUpload.registerKindRecW") },
+ ],
+ };
+
// 다이얼로그 닫을 때 초기화
React.useEffect(() => {
if (!open) {
@@ -104,12 +109,12 @@ export function B4BulkUploadDialog({
const newFiles = files.filter((f) => !existingNames.has(f.name));
if (newFiles.length === 0) {
- toast.error("이미 선택된 파일입니다");
+ toast.error(t("bulkUpload.duplicateFileError"));
return;
}
setSelectedFiles((prev) => [...prev, ...newFiles]);
- toast.success(`${newFiles.length}개 파일이 선택되었습니다`);
+ toast.success(t("bulkUpload.filesSelectedSuccess", { count: newFiles.length }));
};
// Drag & Drop 핸들러
@@ -152,7 +157,7 @@ export function B4BulkUploadDialog({
// 1단계 완료 (설정)
const handleSettingsNext = () => {
if (!registerKind) {
- toast.error("등록종류를 선택하세요");
+ toast.error(t("bulkUpload.selectRegisterKindError"));
return;
}
setCurrentStep("files");
@@ -161,7 +166,7 @@ export function B4BulkUploadDialog({
// 2단계 완료 (파일 선택)
const handleFilesNext = () => {
if (selectedFiles.length === 0) {
- toast.error("파일을 선택해주세요");
+ toast.error(t("bulkUpload.selectFilesError"));
return;
}
setCurrentStep("validation");
@@ -219,7 +224,7 @@ export function B4BulkUploadDialog({
return {
...parseResult,
mappingStatus: "not_found" as const,
- error: "DOLCE 시스템에서 도면을 찾을 수 없습니다",
+ error: t("validation.notFound"),
};
}
@@ -228,7 +233,7 @@ export function B4BulkUploadDialog({
return {
...parseResult,
mappingStatus: "not_found" as const,
- error: "해당 도면번호가 프로젝트에 등록되어 있지 않습니다",
+ error: t("validation.notRegistered"),
};
}
@@ -237,7 +242,7 @@ export function B4BulkUploadDialog({
return {
...parseResult,
mappingStatus: "not_found" as const,
- error: "도면입수(GTT Deliverables)인 도면만 업로드 가능합니다",
+ error: t("validation.notGttDeliverables"),
};
}
@@ -255,7 +260,7 @@ export function B4BulkUploadDialog({
} catch (error) {
console.error("검증 실패:", error);
toast.error(
- error instanceof Error ? error.message : "검증 중 오류가 발생했습니다"
+ error instanceof Error ? error.message : t("bulkUpload.validationError")
);
}
};
@@ -266,69 +271,146 @@ export function B4BulkUploadDialog({
setCurrentStep("uploading");
setShowValidationDialog(false);
- // 진행률 시뮬레이션
- const progressInterval = setInterval(() => {
- setUploadProgress((prev) => {
- if (prev >= 90) {
- clearInterval(progressInterval);
- return prev;
+ try {
+ console.log(`[B4 일괄 업로드] 시작: ${validFiles.length}개 파일`);
+
+ // 파일을 DrawingNo + RevNo로 그룹화
+ const uploadGroups = new Map<
+ string,
+ Array<{
+ file: File;
+ drawingNo: string;
+ revNo: string;
+ fileName: string;
+ registerGroupId: number;
+ }>
+ >();
+
+ validFiles.forEach((fileResult) => {
+ const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`;
+ if (!uploadGroups.has(groupKey)) {
+ uploadGroups.set(groupKey, []);
}
- return prev + 10;
+ uploadGroups.get(groupKey)!.push({
+ file: fileResult.file,
+ drawingNo: fileResult.parsed!.drawingNo,
+ revNo: fileResult.parsed!.revNo,
+ fileName: fileResult.file.name,
+ registerGroupId: fileResult.registerGroupId || 0,
+ });
});
- }, 500);
- try {
- // FormData 생성
- const formData = new FormData();
- formData.append("projectNo", projectNo);
- formData.append("userId", userId);
- formData.append("userName", userName);
- formData.append("userEmail", userEmail);
- formData.append("vendorCode", vendorCode);
- formData.append("registerKind", registerKind); // RegisterKind 추가
-
- // 파일 및 메타데이터 추가
- validFiles.forEach((fileResult, index) => {
- formData.append(`file_${index}`, fileResult.file);
- formData.append(`drawingNo_${index}`, fileResult.parsed!.drawingNo);
- formData.append(`revNo_${index}`, fileResult.parsed!.revNo);
- formData.append(`fileName_${index}`, fileResult.file.name);
- formData.append(
- `registerGroupId_${index}`,
- String(fileResult.registerGroupId || 0)
- );
- });
+ console.log(`[B4 일괄 업로드] ${uploadGroups.size}개 그룹으로 묶임`);
+
+ let successCount = 0;
+ let failCount = 0;
+ let completedGroups = 0;
+
+ // 각 그룹별로 순차 처리
+ for (const [groupKey, files] of uploadGroups.entries()) {
+ const { drawingNo, revNo, registerGroupId } = files[0];
+
+ try {
+ console.log(`[B4 업로드] 그룹 ${groupKey}: ${files.length}개 파일`);
+
+ // 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,
+ });
+
+ if (!uploadResponse.ok) {
+ throw new Error(`파일 업로드 실패: ${uploadResponse.status}`);
+ }
+
+ const uploadResult = await uploadResponse.json();
+
+ if (!uploadResult.success) {
+ throw new Error(uploadResult.error || "파일 업로드 실패");
+ }
+
+ console.log(`[B4 업로드] 그룹 ${groupKey} 파일 업로드 완료`);
+
+ // 3. 상세도면 등록
+ await editDetailDwgReceipt({
+ dwgList: [
+ {
+ Mode: "ADD",
+ Status: "Draft",
+ RegisterId: 0,
+ ProjectNo: projectNo,
+ Discipline: "",
+ DrawingKind: "B4",
+ DrawingNo: drawingNo,
+ DrawingName: "",
+ RegisterGroupId: registerGroupId,
+ RegisterSerialNo: 0,
+ RegisterKind: registerKind,
+ DrawingRevNo: revNo,
+ Category: "TS",
+ Receiver: null,
+ Manager: "",
+ RegisterDesc: "",
+ UploadId: uploadId,
+ RegCompanyCode: vendorCode,
+ },
+ ],
+ userId,
+ userNm: userName,
+ vendorCode,
+ email: userEmail,
+ });
+
+ console.log(`[B4 업로드] 그룹 ${groupKey} 상세도면 등록 완료`);
+
+ successCount += files.length;
+ } catch (error) {
+ console.error(`[B4 업로드] 그룹 ${groupKey} 실패:`, error);
+ failCount += files.length;
+ }
- formData.append("fileCount", String(validFiles.length));
+ // 진행도 업데이트
+ completedGroups++;
+ const progress = Math.round((completedGroups / uploadGroups.size) * 100);
+ setUploadProgress(progress);
+ }
- // 서버 액션 호출
- const result: B4BulkUploadResult = await bulkUploadB4Files(formData);
+ console.log(`[B4 일괄 업로드] ✅ 완료: 성공 ${successCount}, 실패 ${failCount}`);
- clearInterval(progressInterval);
- setUploadProgress(100);
- setUploadResult(result);
+ const result: B4BulkUploadResult = {
+ success: true,
+ successCount,
+ failCount,
+ };
- if (result.success) {
- setCurrentStep("complete");
- toast.success(
- `${result.successCount}/${validFiles.length}개 파일 업로드 완료`
- );
- } else {
- setCurrentStep("files");
- toast.error(result.error || "업로드 실패");
- }
+ setUploadResult(result);
+ setCurrentStep("complete");
+ toast.success(t("bulkUpload.uploadSuccessToast", { successCount, total: validFiles.length }));
} catch (error) {
- console.error("업로드 실패:", error);
+ console.error("[B4 일괄 업로드] 실패:", error);
toast.error(
- error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다"
+ error instanceof Error ? error.message : t("bulkUpload.uploadError")
);
+ setCurrentStep("files");
} finally {
setIsUploading(false);
}
};
const registerKindOptions = drawingUsage
- ? B4_REGISTER_KIND_OPTIONS[drawingUsage] || []
+ ? registerKindOptionsMap[drawingUsage] || []
: [];
const handleDrawingUsageChange = (value: string) => {
@@ -341,11 +423,11 @@ export function B4BulkUploadDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
- <DialogTitle>B4 일괄 업로드</DialogTitle>
+ <DialogTitle>{t("bulkUpload.title")}</DialogTitle>
<DialogDescription>
- {currentStep === "settings" && "업로드 설정을 선택하세요"}
- {currentStep === "files" && "파일명 형식: [버림] [DrawingNo] [RevNo].[확장자] (예: testfile GTT-DE-007 R01.pdf)"}
- {currentStep === "validation" && "파일 검증 중..."}
+ {currentStep === "settings" && t("bulkUpload.stepSettings")}
+ {currentStep === "files" && t("bulkUpload.stepFiles")}
+ {currentStep === "validation" && t("bulkUpload.stepValidation")}
</DialogDescription>
</DialogHeader>
@@ -355,13 +437,13 @@ export function B4BulkUploadDialog({
<>
{/* 도면용도 선택 */}
<div className="space-y-2">
- <Label>도면용도 (Drawing Usage) *</Label>
+ <Label>{t("bulkUpload.drawingUsage")} *</Label>
<Select value={drawingUsage} onValueChange={handleDrawingUsageChange}>
<SelectTrigger>
- <SelectValue placeholder="도면용도를 선택하세요" />
+ <SelectValue placeholder={t("bulkUpload.drawingUsagePlaceholder")} />
</SelectTrigger>
<SelectContent>
- {B4_DRAWING_USAGE_OPTIONS.map((option) => (
+ {drawingUsageOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -372,14 +454,14 @@ export function B4BulkUploadDialog({
{/* 등록종류 선택 */}
<div className="space-y-2">
- <Label>등록종류 (Register Kind) *</Label>
+ <Label>{t("bulkUpload.registerKind")} *</Label>
<Select
value={registerKind}
onValueChange={setRegisterKind}
disabled={!drawingUsage}
>
<SelectTrigger>
- <SelectValue placeholder="등록종류를 선택하세요" />
+ <SelectValue placeholder={t("bulkUpload.registerKindPlaceholder")} />
</SelectTrigger>
<SelectContent>
{registerKindOptions.map((option) => (
@@ -390,7 +472,7 @@ export function B4BulkUploadDialog({
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
- 선택한 등록종류가 모든 파일에 적용됩니다
+ {t("bulkUpload.registerKindNote")}
</p>
</div>
</>
@@ -436,11 +518,11 @@ export function B4BulkUploadDialog({
}`}
>
{isDragging
- ? "파일을 여기에 놓으세요"
- : "클릭하거나 파일을 드래그하여 선택"}
+ ? t("bulkUpload.fileDropHere")
+ : t("bulkUpload.fileSelectArea")}
</p>
<p className="text-xs text-muted-foreground mt-1">
- PDF, DOC, DOCX, XLS, XLSX, DWG, DXF, ZIP
+ {t("bulkUpload.fileTypes")}
</p>
</label>
</div>
@@ -450,17 +532,17 @@ export function B4BulkUploadDialog({
<div className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium">
- 선택된 파일 ({selectedFiles.length}개)
+ {t("bulkUpload.selectedFiles", { count: selectedFiles.length })}
</h4>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedFiles([])}
>
- 전체 제거
+ {t("bulkUpload.removeAll")}
</Button>
</div>
- <div className="max-h-48 overflow-auto space-y-2">
+ <div className="max-h-60 overflow-y-auto space-y-2">
{selectedFiles.map((file, index) => (
<div
key={index}
@@ -477,7 +559,7 @@ export function B4BulkUploadDialog({
size="sm"
onClick={() => handleRemoveFile(index)}
>
- 제거
+ {t("bulkUpload.removeFile")}
</Button>
</div>
))}
@@ -492,7 +574,7 @@ export function B4BulkUploadDialog({
<div className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
<p className="text-sm text-muted-foreground">
- 파일 검증 중입니다...
+ {t("bulkUpload.validating")}
</p>
</div>
)}
@@ -502,17 +584,23 @@ export function B4BulkUploadDialog({
<div className="space-y-6 py-8">
<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">파일 업로드 중...</h3>
+ <h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploading")}</h3>
<p className="text-sm text-muted-foreground">
- 잠시만 기다려주세요
+ {t("bulkUpload.uploadingWait")}
</p>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
- <span>진행률</span>
+ <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>
</div>
)}
@@ -522,16 +610,16 @@ export function B4BulkUploadDialog({
<div className="space-y-6 py-8">
<div className="flex flex-col items-center">
<CheckCircle2 className="h-16 w-16 text-green-500 mb-4" />
- <h3 className="text-lg font-semibold mb-2">업로드 완료!</h3>
+ <h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploadComplete")}</h3>
<p className="text-sm text-muted-foreground">
- {uploadResult.successCount}개 파일이 성공적으로 업로드되었습니다
+ {t("bulkUpload.uploadSuccessMessage", { count: uploadResult.successCount })}
</p>
</div>
{uploadResult.failCount && uploadResult.failCount > 0 && (
<div className="bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
- {uploadResult.failCount}개 파일 업로드 실패
+ {t("bulkUpload.uploadFailMessage", { count: uploadResult.failCount })}
</p>
</div>
)}
@@ -543,7 +631,7 @@ export function B4BulkUploadDialog({
onUploadComplete?.();
}}
>
- 확인
+ {t("bulkUpload.confirmButton")}
</Button>
</div>
</div>
@@ -559,13 +647,13 @@ export function B4BulkUploadDialog({
variant="outline"
onClick={() => onOpenChange(false)}
>
- 취소
+ {t("bulkUpload.cancelButton")}
</Button>
<Button
onClick={handleSettingsNext}
disabled={!registerKind}
>
- 다음
+ {t("bulkUpload.nextButton")}
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
</>
@@ -578,13 +666,13 @@ export function B4BulkUploadDialog({
onClick={() => setCurrentStep("settings")}
>
<ChevronLeft className="mr-2 h-4 w-4" />
- 이전
+ {t("bulkUpload.previousButton")}
</Button>
<Button
onClick={handleFilesNext}
disabled={selectedFiles.length === 0}
>
- 검증 시작
+ {t("bulkUpload.validateButton")}
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
</>
@@ -601,6 +689,7 @@ export function B4BulkUploadDialog({
validationResults={validationResults}
onConfirmUpload={handleConfirmUpload}
isUploading={isUploading}
+ lng={lng}
/>
</>
);
diff --git a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
index b274d604..f3a7c70a 100644
--- a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
+++ b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
@@ -39,8 +39,11 @@ interface B4UploadValidationDialogProps {
/**
* B4 파일명 검증 함수
- * 형식: [버림] [DrawingNo] [RevNo].[확장자]
- * 예시: "testfile GTT-DE-007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01"
+ * 형식: [버림] [문서번호토큰1] [문서번호토큰2] ... [리비전번호].[확장자]
+ * 예시: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01"
+ * - 첫 번째 토큰은 버림
+ * - 마지막 토큰은 RevNo
+ * - 중간 토큰들을 "-"로 연결하여 DrawingNo 생성
*/
export function validateB4FileName(fileName: string): {
valid: boolean;
@@ -57,23 +60,25 @@ export function validateB4FileName(fileName: string): {
};
}
- const extension = fileName.substring(lastDotIndex + 1);
const nameWithoutExt = fileName.substring(0, lastDotIndex);
// 공백으로 분리
const parts = nameWithoutExt.split(" ").filter(p => p.trim() !== "");
- // 최소 3개 파트 필요: [버림], DrawingNo, RevNo
+ // 최소 3개 파트 필요: [버림], [문서번호토큰], [RevNo]
if (parts.length < 3) {
return {
valid: false,
- error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [DrawingNo] [RevNo].[확장자]`,
+ error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [문서번호토큰들...] [RevNo].[확장자]`,
};
}
// 첫 번째 토큰은 버림
- const drawingNo = parts[1];
- const revNo = parts[2];
+ // 마지막 토큰은 RevNo
+ // 중간 토큰들을 "-"로 연결하여 DrawingNo 생성
+ const revNo = parts[parts.length - 1];
+ const drawingTokens = parts.slice(1, parts.length - 1);
+ const drawingNo = drawingTokens.join("-");
// 필수 항목이 비어있지 않은지 확인
if (!drawingNo || drawingNo.trim() === "") {
@@ -307,16 +312,19 @@ export function B4UploadValidationDialog({
📋 올바른 파일명 형식
</div>
<code className="text-xs text-blue-700 dark:text-blue-300">
- [버림] [DrawingNo] [RevNo].[확장자]
+ [버림] [문서번호토큰1] [문서번호토큰2] ... [RevNo].[확장자]
</code>
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
- 예: testfile GTT-DE-007 R01.pdf
+ 예: 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">
- ※ 첫 번째 단어는 무시되며, 공백으로 구분됩니다
+ ※ 첫 번째 단어는 무시됩니다
</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)가 됩니다
</div>
</div>
</div>
diff --git a/lib/dolce/dialogs/detail-drawing-dialog.tsx b/lib/dolce/dialogs/detail-drawing-dialog.tsx
index a06c9688..d9df58db 100644
--- a/lib/dolce/dialogs/detail-drawing-dialog.tsx
+++ b/lib/dolce/dialogs/detail-drawing-dialog.tsx
@@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Plus, RefreshCw, Upload, Loader2 } from "lucide-react";
import { toast } from "sonner";
+import { useTranslation } from "@/i18n/client";
import {
UnifiedDwgReceiptItem,
DetailDwgReceiptItem,
@@ -20,7 +21,7 @@ import {
fetchFileInfoList,
} from "../actions";
import { DrawingListTable } from "../table/drawing-list-table";
-import { detailDrawingColumns } from "../table/detail-drawing-columns";
+import { createDetailDrawingColumns } from "../table/detail-drawing-columns";
import { createFileListColumns } from "../table/file-list-columns";
import { AddDetailDrawingDialog } from "./add-detail-drawing-dialog";
import { UploadFilesToDetailDialog } from "./upload-files-to-detail-dialog";
@@ -34,6 +35,7 @@ interface DetailDrawingDialogProps {
userName: string;
userEmail: string;
drawingKind: "B3" | "B4";
+ lng: string;
}
export function DetailDrawingDialog({
@@ -45,7 +47,9 @@ export function DetailDrawingDialog({
userName,
userEmail,
drawingKind,
+ lng,
}: DetailDrawingDialogProps) {
+ const { t } = useTranslation(lng, "dolce");
const [detailDrawings, setDetailDrawings] = useState<DetailDwgReceiptItem[]>([]);
const [selectedDetail, setSelectedDetail] = useState<DetailDwgReceiptItem | null>(null);
const [files, setFiles] = useState<FileInfoItem[]>([]);
@@ -75,11 +79,11 @@ export function DetailDrawingDialog({
}
} catch (error) {
console.error("상세도면 로드 실패:", error);
- toast.error("상세도면 로드에 실패했습니다");
+ toast.error(t("detailDialog.detailLoadError"));
} finally {
setIsLoading(false);
}
- }, [drawing, selectedDetail]);
+ }, [drawing, selectedDetail, t]);
// 파일 목록 로드
const loadFiles = useCallback(async () => {
@@ -94,11 +98,11 @@ export function DetailDrawingDialog({
setFiles(data);
} catch (error) {
console.error("파일 목록 로드 실패:", error);
- toast.error("파일 목록 로드에 실패했습니다");
+ toast.error(t("detailDialog.fileLoadError"));
} finally {
setIsLoadingFiles(false);
}
- }, [selectedDetail]);
+ }, [selectedDetail, t]);
// 다이얼로그 열릴 때 데이터 로드
useEffect(() => {
@@ -120,7 +124,7 @@ export function DetailDrawingDialog({
const handleDownload = async (file: FileInfoItem) => {
try {
- toast.info("파일 다운로드를 준비 중입니다...");
+ toast.info(t("detailDialog.downloadPreparing"));
// 파일 생성자의 userId를 사용하여 다운로드
const response = await fetch("/api/dolce/download", {
@@ -136,7 +140,7 @@ export function DetailDrawingDialog({
});
if (!response.ok) {
- throw new Error("파일 다운로드 실패");
+ throw new Error(t("detailDialog.downloadError"));
}
const blob = await response.blob();
@@ -149,10 +153,10 @@ export function DetailDrawingDialog({
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
- toast.success("파일 다운로드가 완료되었습니다");
+ toast.success(t("detailDialog.downloadSuccess"));
} catch (error) {
console.error("파일 다운로드 실패:", error);
- toast.error("파일 다운로드에 실패했습니다");
+ toast.error(t("detailDialog.downloadError"));
}
};
@@ -170,7 +174,7 @@ export function DetailDrawingDialog({
loadFiles();
};
- const fileColumns = createFileListColumns({ onDownload: handleDownload });
+ const fileColumns = createFileListColumns({ onDownload: handleDownload, lng });
// RegisterId + UploadId 조합으로 고유 ID 생성
const getDetailDrawingId = (detail: DetailDwgReceiptItem) => {
@@ -188,10 +192,15 @@ export function DetailDrawingDialog({
<DialogContent className="max-w-[95vw] h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex flex-col gap-1">
- <span>상세도면 정보</span>
+ <span>{t("detailDialog.title")}</span>
{drawing && (
<span className="text-sm font-normal text-muted-foreground">
- {drawing.DrawingNo} | 프로젝트: {drawing.ProjectNo} | Discipline: {drawing.Discipline} | 종류: {drawing.DrawingKind}
+ {t("detailDialog.subtitle", {
+ drawingNo: drawing.DrawingNo,
+ projectNo: drawing.ProjectNo,
+ discipline: drawing.Discipline,
+ drawingKind: drawing.DrawingKind
+ })}
</span>
)}
</DialogTitle>
@@ -201,7 +210,7 @@ export function DetailDrawingDialog({
{/* 상단: 상세도면 리스트 */}
<Card className="flex-1 overflow-hidden flex flex-col">
<CardHeader className="flex-row items-center justify-between py-3">
- <CardTitle className="text-base">상세도면 목록</CardTitle>
+ <CardTitle className="text-base">{t("detailDialog.detailListTitle")}</CardTitle>
<div className="flex gap-2">
<Button
variant="outline"
@@ -210,7 +219,7 @@ export function DetailDrawingDialog({
disabled={isLoading}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
- 새로고침
+ {t("detailDialog.refreshButton")}
</Button>
{canAddDetailDrawing && (
<Button
@@ -219,14 +228,14 @@ export function DetailDrawingDialog({
onClick={() => setAddDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
- 상세도면 추가
+ {t("detailDialog.addDetailButton")}
</Button>
)}
</div>
</CardHeader>
<CardContent className="flex-1 overflow-y-auto p-4">
<DrawingListTable<DetailDwgReceiptItem, unknown>
- columns={detailDrawingColumns}
+ columns={createDetailDrawingColumns(lng, t)}
data={detailDrawings}
onRowClick={setSelectedDetail}
selectedRow={selectedDetail || undefined}
@@ -239,8 +248,8 @@ export function DetailDrawingDialog({
<Card className="flex-1 overflow-hidden flex flex-col">
<CardHeader className="flex-row items-center justify-between py-3">
<CardTitle className="text-base">
- 첨부파일 목록
- {selectedDetail && ` - Rev. ${selectedDetail.DrawingRevNo}`}
+ {t("detailDialog.fileListTitle")}
+ {selectedDetail && t("detailDialog.fileListSubtitle", { revNo: selectedDetail.DrawingRevNo })}
</CardTitle>
{selectedDetail && canAddDetailDrawing && (
<Button
@@ -249,20 +258,20 @@ export function DetailDrawingDialog({
onClick={() => setUploadFilesDialogOpen(true)}
>
<Upload className="h-4 w-4 mr-2" />
- 파일 업로드
+ {t("detailDialog.uploadFilesButton")}
</Button>
)}
</CardHeader>
<CardContent className="flex-1 overflow-y-auto p-4">
{!selectedDetail ? (
<div className="h-full flex items-center justify-center text-muted-foreground">
- 상세도면을 선택하세요
+ {t("detailDialog.selectDetailDrawing")}
</div>
) : isLoadingFiles ? (
<div className="space-y-4">
<div className="flex items-center justify-center gap-2 text-muted-foreground py-8">
<Loader2 className="h-5 w-5 animate-spin" />
- <span>Loading files...</span>
+ <span>{t("detailDialog.loadingFiles")}</span>
</div>
<div className="space-y-2">
<Skeleton className="h-10 w-full" />
@@ -292,6 +301,7 @@ export function DetailDrawingDialog({
userEmail={userEmail}
onComplete={handleAddComplete}
drawingKind={drawingKind}
+ lng={lng}
/>
{selectedDetail && (
@@ -303,6 +313,7 @@ export function DetailDrawingDialog({
revNo={selectedDetail.DrawingRevNo}
userId={userId}
onUploadComplete={handleUploadComplete}
+ lng={lng}
/>
)}
</>
diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
index af73aea6..09f68614 100644
--- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
+++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
@@ -26,6 +26,7 @@ interface UploadFilesToDetailDialogProps {
revNo: string;
userId: string;
onUploadComplete?: () => void;
+ lng?: string; // i18n support
}
export function UploadFilesToDetailDialog({
@@ -181,7 +182,7 @@ export function UploadFilesToDetailDialog({
전체 제거
</Button>
</div>
- <div className="max-h-48 overflow-auto space-y-2">
+ <div className="max-h-60 overflow-y-auto space-y-2">
{selectedFiles.map((file, index) => (
<div
key={index}