summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-12 17:49:57 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-12 17:49:57 +0900
commitf4b1a770184e8647e0b3042a52a1bcc6f9cf00ce (patch)
treed76700d8f52fa893afa816cf60bd3620f3032fe7
parentd3ff18a2320eeb400dc5d18588490c775bff4820 (diff)
(김준회) SWP: 파일 업로드시 Stage 검증 추가, DOC_CLASS 관리 단순화 (코드 제거), DOC_CLASS 추가시 검증(A-Z0-9) 처리
-rw-r--r--app/api/swp/upload/route.ts54
-rw-r--r--lib/docu-list-rule/document-class/service.ts132
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx21
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-columns.tsx43
-rw-r--r--lib/swp/table/swp-table-toolbar.tsx81
-rw-r--r--lib/swp/table/swp-upload-validation-dialog.tsx55
-rw-r--r--lib/vendor-document-list/plant/document-stage-dialogs.tsx12
-rw-r--r--lib/vendor-document-list/plant/document-stages-service.ts9
-rw-r--r--lib/vendor-document-list/plant/excel-import-stage.tsx12
9 files changed, 246 insertions, 173 deletions
diff --git a/app/api/swp/upload/route.ts b/app/api/swp/upload/route.ts
index fcb2aa71..71c88cec 100644
--- a/app/api/swp/upload/route.ts
+++ b/app/api/swp/upload/route.ts
@@ -3,12 +3,11 @@ import * as fs from "fs/promises";
import * as path from "path";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
-import { fetchGetVDRDocumentList, fetchGetExternalInboxList } from "@/lib/swp/api-client";
import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils";
// API Route 설정
export const runtime = "nodejs";
-export const maxDuration = 300; // 5분 타임아웃 (대용량 파일 업로드 대응)
+export const maxDuration = 3600; // 1시간 타임아웃 (대용량 파일 업로드 대응)
interface InBoxFileInfo {
CPY_CD: string;
@@ -93,45 +92,6 @@ function generateTimestamp(): string {
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
-/**
- * CPY_CD 조회 (API 기반)
- * GetVDRDocumentList API를 호출하여 해당 프로젝트/벤더의 CPY_CD를 조회
- */
-async function getCpyCdForVendor(projNo: string, vndrCd: string): Promise<string> {
- try {
- console.log(`[getCpyCdForVendor] API 조회 시작: projNo=${projNo}, vndrCd=${vndrCd}`);
-
- // GetVDRDocumentList API 호출 (벤더 필터 적용)
- const documents = await fetchGetVDRDocumentList({
- proj_no: projNo,
- doc_gb: "V",
- vndrCd: vndrCd,
- });
-
- console.log(`[getCpyCdForVendor] API 조회 완료: ${documents.length}개 문서`);
-
- if (!documents || documents.length === 0) {
- throw new Error(
- `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 할당된 문서가 없습니다.`
- );
- }
-
- // 첫 번째 문서에서 CPY_CD 추출
- const cpyCd = documents[0].CPY_CD;
-
- if (!cpyCd) {
- throw new Error(
- `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 해당하는 회사 코드(CPY_CD)를 찾을 수 없습니다.`
- );
- }
-
- console.log(`[getCpyCdForVendor] CPY_CD 확인: ${cpyCd}`);
- return cpyCd;
- } catch (error) {
- console.error("[getCpyCdForVendor] 오류:", error);
- throw error;
- }
-}
/**
* SaveInBoxList API 호출
@@ -225,10 +185,8 @@ export async function POST(request: NextRequest) {
);
}
- // CPY_CD 조회
- console.log(`[upload] CPY_CD 조회: projNo=${projNo}, vndrCd=${vndrCd}`);
- const cpyCd = await getCpyCdForVendor(projNo, vndrCd);
- console.log(`[upload] CPY_CD: ${cpyCd}`);
+ // vndrCd를 CPY_CD로 사용
+ console.log(`[upload] vndrCd를 CPY_CD로 사용: ${vndrCd}`);
const files = formData.getAll("files") as File[];
@@ -259,7 +217,7 @@ export async function POST(request: NextRequest) {
console.log(`[upload] 파일명 파싱:`, parsed);
// 네트워크 경로 생성 (timestamp를 경로에만 사용)
- const networkPath = path.join(swpMountDir, projNo, cpyCd, uploadTimestamp, file.name);
+ const networkPath = path.join(swpMountDir, projNo, vndrCd, uploadTimestamp, file.name);
// 파일 중복 체크
try {
@@ -326,10 +284,10 @@ export async function POST(request: NextRequest) {
});
// InBox 파일 정보 준비 (FLD_PATH에 업로드 timestamp 사용)
- const fldPath = `\\${projNo}\\${cpyCd}\\\\${uploadTimestamp}`;
+ const fldPath = `\\${projNo}\\${vndrCd}\\\\${uploadTimestamp}`;
inBoxFileInfos.push({
- CPY_CD: cpyCd,
+ CPY_CD: vndrCd,
FILE_NM: file.name,
OFDC_NO: null,
PROJ_NO: projNo,
diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts
index d92f3a95..9d3ff23a 100644
--- a/lib/docu-list-rule/document-class/service.ts
+++ b/lib/docu-list-rule/document-class/service.ts
@@ -149,51 +149,16 @@ export async function createDocumentClassCodeGroup(input: {
description?: string
}) {
try {
- // Value 자동 변환: "A", "AB", "A Class", "A CLASS" 등을 "A Class", "AB Class" 형태로 변환
- const formatValue = (input: string): string => {
- // 공백 제거 및 대소문자 정규화
- const cleaned = input.trim().toLowerCase()
-
- // "class"가 포함되어 있으면 제거
- const withoutClass = cleaned.replace(/\s*class\s*/g, '')
-
- // 알파벳과 숫자만 추출
- const letters = withoutClass.replace(/[^a-z0-9]/g, '')
-
- if (letters.length === 0) {
- return input.trim() // 변환할 수 없으면 원본 반환
- }
-
- // 첫 글자를 대문자로 변환하고 "Class" 추가
- return letters.charAt(0).toUpperCase() + letters.slice(1) + " Class"
- }
-
- const formattedValue = formatValue(input.value)
-
- // 해당 프로젝트의 자동으로 code 생성 (예: "DOC_CLASS_001", "DOC_CLASS_002" 등)
- const existingClasses = await db
- .select({ code: documentClasses.code })
- .from(documentClasses)
- .where(eq(documentClasses.projectId, input.projectId)) // projectId로 변경
- .orderBy(desc(documentClasses.code))
-
- let newCode = "DOC_CLASS_001"
- if (existingClasses.length > 0) {
- const lastClass = existingClasses[0]
- if (lastClass.code) {
- const lastNumber = parseInt(lastClass.code.replace("DOC_CLASS_", "")) || 0
- newCode = `DOC_CLASS_${String(lastNumber + 1).padStart(3, '0')}`
- }
- }
+ // Value는 1자리 대문자 알파벳 그대로 저장 (API DOC_CLASS 전송용)
+ const formattedValue = input.value.trim().toUpperCase()
const [newDocumentClass] = await db
.insert(documentClasses)
.values({
- projectId: input.projectId, // projectId로 변경
- code: newCode,
- value: formattedValue,
+ projectId: input.projectId,
+ value: formattedValue, // "A", "B", "C" 등 1자리
description: input.description || "",
- codeGroupId: null, // Code Group 연결 제거
+ codeGroupId: null,
isActive: true,
})
.returning({ id: documentClasses.id })
@@ -222,31 +187,13 @@ export async function updateDocumentClassCodeGroup(input: {
description?: string
}) {
try {
- // Value 자동 변환: "A", "AB", "A Class", "A CLASS" 등을 "A Class", "AB Class" 형태로 변환
- const formatValue = (value: string): string => {
- // 공백 제거 및 대소문자 정규화
- const cleaned = value.trim().toLowerCase()
-
- // "class"가 포함되어 있으면 제거
- const withoutClass = cleaned.replace(/\s*class\s*/g, '')
-
- // 알파벳과 숫자만 추출
- const letters = withoutClass.replace(/[^a-z0-9]/g, '')
-
- if (letters.length === 0) {
- return value.trim() // 변환할 수 없으면 원본 반환
- }
-
- // 첫 글자를 대문자로 변환하고 "Class" 추가
- return letters.charAt(0).toUpperCase() + letters.slice(1) + " Class"
- }
-
- const formattedValue = formatValue(input.value)
+ // Value는 1자리 대문자 알파벳 그대로 저장 (API DOC_CLASS 전송용)
+ const formattedValue = input.value.trim().toUpperCase()
const [updatedDocumentClass] = await db
.update(documentClasses)
.set({
- value: formattedValue,
+ value: formattedValue, // "A", "B", "C" 등 1자리
description: input.description || "",
updatedAt: new Date(),
})
@@ -630,4 +577,67 @@ export async function getProjectKindScheduleSetting(projectCode: string): Promis
console.error('Error fetching schedule settings:', error)
return []
}
+}
+
+/**
+ * 프로젝트의 Document Class와 해당 Stage 옵션 매핑 조회
+ * @param projectCode 프로젝트 코드 (예: "SN2190")
+ * @returns Document Class별 허용 Stage 목록 맵
+ */
+export async function getProjectDocumentClassStages(projectCode: string): Promise<Record<string, string[]>> {
+ try {
+ // 1. 프로젝트 ID 조회
+ const project = await db
+ .select({ id: projects.id })
+ .from(projects)
+ .where(eq(projects.code, projectCode))
+ .limit(1);
+
+ if (!project.length) {
+ console.warn(`[getProjectDocumentClassStages] 프로젝트를 찾을 수 없습니다: ${projectCode}`);
+ return {};
+ }
+
+ const projectId = project[0].id;
+
+ // 2. 프로젝트의 모든 Document Class와 옵션 조회
+ const documentClassesWithOptions = await db
+ .select({
+ docClassValue: documentClasses.value,
+ optionCode: documentClassOptions.optionCode,
+ })
+ .from(documentClasses)
+ .leftJoin(
+ documentClassOptions,
+ eq(documentClasses.id, documentClassOptions.documentClassId)
+ )
+ .where(
+ and(
+ eq(documentClasses.projectId, projectId),
+ eq(documentClasses.isActive, true),
+ eq(documentClassOptions.isActive, true)
+ )
+ )
+ .orderBy(documentClasses.value, documentClassOptions.sdq);
+
+ // 3. Document Class별로 Stage 목록 그룹핑
+ const stageMap: Record<string, string[]> = {};
+
+ for (const row of documentClassesWithOptions) {
+ if (!row.docClassValue || !row.optionCode) continue;
+
+ if (!stageMap[row.docClassValue]) {
+ stageMap[row.docClassValue] = [];
+ }
+
+ stageMap[row.docClassValue].push(row.optionCode);
+ }
+
+ console.log(`[getProjectDocumentClassStages] ${projectCode}: ${Object.keys(stageMap).length}개 Document Class, 총 ${Object.values(stageMap).flat().length}개 Stage 옵션`);
+
+ return stageMap;
+ } catch (error) {
+ console.error('[getProjectDocumentClassStages] 오류:', error);
+ return {};
+ }
} \ No newline at end of file
diff --git a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx
index e2cfc39e..6e8ac686 100644
--- a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx
@@ -32,7 +32,10 @@ import { createDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-clas
import { useParams } from "next/navigation"
const createDocumentClassSchema = z.object({
- value: z.string().min(1, "Value는 필수입니다."),
+ value: z.string()
+ .min(1, "Value는 필수입니다.")
+ .max(1, "Value는 1자리만 입력 가능합니다. (예: A, B, 0, 1)")
+ .regex(/^[A-Z0-9]$/, "대문자 알파벳 또는 숫자 1자리만 입력 가능합니다. (예: A, B, 0, 1)"),
description: z.string().optional(),
})
@@ -117,8 +120,17 @@ export function DocumentClassAddDialog({
<FormItem>
<FormLabel>Value *</FormLabel>
<FormControl>
- <Input {...field} placeholder="예: A Class" />
+ <Input
+ {...field}
+ placeholder="예: A"
+ maxLength={1}
+ className="uppercase"
+ onChange={(e) => field.onChange(e.target.value.toUpperCase())}
+ />
</FormControl>
+ <div className="text-xs text-muted-foreground mt-1">
+ 💡 대문자 알파벳 또는 숫자 1자리 (A, B, 0, 1 등) - API DOC_CLASS로 전송됩니다
+ </div>
<FormMessage />
</FormItem>
)}
@@ -131,8 +143,11 @@ export function DocumentClassAddDialog({
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
- <Input {...field} placeholder="예: A Class Description (선택사항)" />
+ <Input {...field} placeholder="예: General Documents (선택사항)" />
</FormControl>
+ <div className="text-xs text-muted-foreground mt-1">
+ 선택사항: Document Class에 대한 추가 설명
+ </div>
<FormMessage />
</FormItem>
)}
diff --git a/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx
index 8c391def..9d8d91e0 100644
--- a/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx
@@ -107,43 +107,28 @@ export function getColumns({ setRowAction, onDetail }: GetColumnsProps): ColumnD
// ----------------------------------------------------------------
const dataColumns: ColumnDef<typeof documentClasses.$inferSelect>[] = [
{
- accessorKey: "code",
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="코드" />
- ),
- meta: {
- excelHeader: "코드",
- type: "text",
- },
- cell: ({ row }) => row.getValue("code") ?? "",
- minSize: 80
- },
- {
accessorKey: "value",
enableResizing: true,
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="값" />
+ <DataTableColumnHeaderSimple column={column} title="클래스" />
),
meta: {
- excelHeader: "값",
+ excelHeader: "클래스",
type: "text",
},
- cell: ({ row }) => row.getValue("value") ?? "",
- minSize: 80
- },
- {
- accessorKey: "description",
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="설명" />
- ),
- meta: {
- excelHeader: "설명",
- type: "text",
+ cell: ({ row }) => {
+ const value = row.getValue("value") as string
+ const description = row.getValue("description") as string
+ return (
+ <div className="flex items-center gap-2">
+ <span className="font-mono font-bold text-lg">{value}</span>
+ {description && (
+ <span className="text-muted-foreground text-sm">- {description}</span>
+ )}
+ </div>
+ )
},
- cell: ({ row }) => row.getValue("description") ?? "",
- minSize: 80
+ minSize: 250
},
{
diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx
index add69666..013b4a13 100644
--- a/lib/swp/table/swp-table-toolbar.tsx
+++ b/lib/swp/table/swp-table-toolbar.tsx
@@ -19,6 +19,8 @@ import {
validateFileName
} from "./swp-upload-validation-dialog";
import { SwpUploadedFilesDialog } from "./swp-uploaded-files-dialog";
+import { getProjectDocumentClassStages } from "@/lib/docu-list-rule/document-class/service";
+import type { DocumentListItem } from "@/lib/swp/document-service";
interface SwpTableFilters {
docNo?: string;
@@ -38,7 +40,7 @@ interface SwpTableToolbarProps {
vendorCode?: string;
droppedFiles?: File[];
onFilesProcessed?: () => void;
- documents?: Array<{ OWN_DOC_NO: string | null }>; // 업로드 권한 검증용 문서 목록 (OWN_DOC_NO 기준)
+ documents?: DocumentListItem[]; // 업로드 권한 검증 + DOC_TYPE 확인용 문서 목록
userId?: string; // 파일 취소 시 필요
}
@@ -80,6 +82,10 @@ export function SwpTableToolbar({
}>>([]);
const [showValidationDialog, setShowValidationDialog] = useState(false);
+ // Document Class-Stage 매핑 (프로젝트별)
+ const [documentClassStages, setDocumentClassStages] = useState<Record<string, string[]>>({});
+ const [isLoadingDocClassStages, setIsLoadingDocClassStages] = useState(false);
+
/**
* 업로드 가능한 문서번호 목록 추출 (OWN_DOC_NO 기준)
*/
@@ -90,11 +96,62 @@ export function SwpTableToolbar({
}, [documents]);
/**
+ * 문서번호 → DOC_TYPE 매핑 (Stage 검증용)
+ */
+ const docNoToDocTypeMap = useMemo(() => {
+ const map: Record<string, string> = {};
+ for (const doc of documents) {
+ if (doc.OWN_DOC_NO && doc.DOC_TYPE) {
+ map[doc.OWN_DOC_NO] = doc.DOC_TYPE;
+ }
+ }
+ return map;
+ }, [documents]);
+
+ /**
* 벤더 모드 여부 (벤더 코드가 있으면 벤더 모드)
*/
const isVendorMode = !!vendorCode;
/**
+ * 프로젝트 변경 시 Document Class-Stage 매핑 로드
+ */
+ useEffect(() => {
+ if (!projNo) {
+ setDocumentClassStages({});
+ return;
+ }
+
+ let isCancelled = false;
+
+ const loadDocumentClassStages = async () => {
+ try {
+ setIsLoadingDocClassStages(true);
+ const stages = await getProjectDocumentClassStages(projNo);
+ if (!isCancelled) {
+ setDocumentClassStages(stages);
+ console.log(`[SwpTableToolbar] Document Class-Stage 매핑 로드 완료:`, stages);
+ }
+ } catch (error) {
+ if (!isCancelled) {
+ console.error('[SwpTableToolbar] Document Class-Stage 매핑 로드 실패:', error);
+ setDocumentClassStages({});
+ }
+ } finally {
+ if (!isCancelled) {
+ setIsLoadingDocClassStages(false);
+ }
+ }
+ };
+
+ loadDocumentClassStages();
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [projNo]);
+
+ /**
* 드롭된 파일 처리 - useEffect로 감지하여 자동 검증
*/
useEffect(() => {
@@ -120,9 +177,15 @@ export function SwpTableToolbar({
return;
}
- // 파일명 검증 (문서번호 권한 포함)
+ // 파일명 검증 (문서번호 권한 + Stage 검증 포함)
const results = droppedFiles.map((file) => {
- const validation = validateFileName(file.name, availableDocNos, isVendorMode);
+ const validation = validateFileName(
+ file.name,
+ availableDocNos,
+ isVendorMode,
+ docNoToDocTypeMap,
+ documentClassStages
+ );
return {
file,
valid: validation.valid,
@@ -135,7 +198,7 @@ export function SwpTableToolbar({
setShowValidationDialog(true);
onFilesProcessed?.();
}
- }, [droppedFiles, projNo, vendorCode, toast, onFilesProcessed, availableDocNos, isVendorMode]);
+ }, [droppedFiles, projNo, vendorCode, toast, onFilesProcessed, availableDocNos, isVendorMode, docNoToDocTypeMap, documentClassStages]);
/**
* 파일 업로드 핸들러
@@ -171,9 +234,15 @@ export function SwpTableToolbar({
return;
}
- // 각 파일의 파일명 검증 (문서번호 권한 포함)
+ // 각 파일의 파일명 검증 (문서번호 권한 + Stage 검증 포함)
const results = Array.from(selectedFiles).map((file) => {
- const validation = validateFileName(file.name, availableDocNos, isVendorMode);
+ const validation = validateFileName(
+ file.name,
+ availableDocNos,
+ isVendorMode,
+ docNoToDocTypeMap,
+ documentClassStages
+ );
return {
file,
valid: validation.valid,
diff --git a/lib/swp/table/swp-upload-validation-dialog.tsx b/lib/swp/table/swp-upload-validation-dialog.tsx
index 8e786c8b..ef48f0c6 100644
--- a/lib/swp/table/swp-upload-validation-dialog.tsx
+++ b/lib/swp/table/swp-upload-validation-dialog.tsx
@@ -43,11 +43,15 @@ interface SwpUploadValidationDialogProps {
* @param fileName 검증할 파일명
* @param availableDocNos 업로드 가능한 문서번호 목록 (선택)
* @param isVendorMode 벤더 모드인지 여부 (true인 경우 문서번호 검증 필수)
+ * @param docNoToDocTypeMap 문서번호 → DOC_TYPE 매핑 (Stage 검증용)
+ * @param documentClassStages Document Class → 허용 Stage 목록 매핑
*/
export function validateFileName(
fileName: string,
availableDocNos?: string[],
- isVendorMode?: boolean
+ isVendorMode?: boolean,
+ docNoToDocTypeMap?: Record<string, string>,
+ documentClassStages?: Record<string, string[]>
): {
valid: boolean;
parsed?: {
@@ -134,12 +138,36 @@ export function validateFileName(
}
}
+ // Stage 검증 (DOC_TYPE별 허용 Stage 확인)
+ const trimmedDocNo = ownDocNo.trim();
+ const trimmedStage = stage.trim();
+
+ if (docNoToDocTypeMap && documentClassStages) {
+ const docType = docNoToDocTypeMap[trimmedDocNo];
+
+ if (docType) {
+ const allowedStages = documentClassStages[docType];
+
+ if (allowedStages && allowedStages.length > 0) {
+ // 허용된 Stage 목록이 있는 경우에만 검증
+ if (!allowedStages.includes(trimmedStage)) {
+ return {
+ valid: false,
+ error: `문서 '${trimmedDocNo}'의 Document Class '${docType}'에서 Stage '${trimmedStage}'는 허용되지 않습니다. 허용된 Stage: ${allowedStages.join(", ")}`,
+ };
+ }
+ }
+ // allowedStages가 비어있으면 Stage 검증을 스킵 (설정되지 않은 경우)
+ }
+ // docType이 없으면 Stage 검증을 스킵 (문서 정보가 없는 경우)
+ }
+
return {
valid: true,
parsed: {
- ownDocNo: ownDocNo.trim(),
+ ownDocNo: trimmedDocNo,
revNo: revNo.trim(),
- stage: stage.trim(),
+ stage: trimmedStage,
fileName: customFileName.trim(),
extension,
},
@@ -314,7 +342,7 @@ export function SwpUploadValidationDialog({
{/* 형식 안내 */}
<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">
- 올바른 파일명 형식
+ 📋 올바른 파일명 형식
</div>
<code className="text-xs text-blue-700 dark:text-blue-300">
[OWN_DOC_NO]_[REV_NO]_[STAGE].[확장자]
@@ -329,13 +357,18 @@ export function SwpUploadValidationDialog({
※ 파일명에는 언더스코어(_)가 포함될 수 있습니다.
</div>
{isVendorMode && (
- <div className="text-xs text-blue-600 dark:text-blue-400 mt-2 pt-2 border-t border-blue-200 dark:border-blue-800">
- {availableDocNos.length > 0 ? (
- <>ℹ️ 업로드 가능한 문서: {availableDocNos.length}개</>
- ) : (
- <>⚠️ 할당된 문서가 없습니다</>
- )}
- </div>
+ <>
+ <div className="text-xs text-blue-600 dark:text-blue-400 mt-2 pt-2 border-t border-blue-200 dark:border-blue-800">
+ {availableDocNos.length > 0 ? (
+ <>ℹ️ 업로드 가능한 문서: {availableDocNos.length}개</>
+ ) : (
+ <>⚠️ 할당된 문서가 없습니다</>
+ )}
+ </div>
+ <div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
+ ⚠️ 각 문서의 Document Class에 정의된 Stage만 사용할 수 있습니다.
+ </div>
+ </>
)}
</div>
</div>
diff --git a/lib/vendor-document-list/plant/document-stage-dialogs.tsx b/lib/vendor-document-list/plant/document-stage-dialogs.tsx
index a738eba9..0c972658 100644
--- a/lib/vendor-document-list/plant/document-stage-dialogs.tsx
+++ b/lib/vendor-document-list/plant/document-stage-dialogs.tsx
@@ -374,9 +374,9 @@ export function AddDocumentDialog({
const shiDocNumber = shiType ? generateShiPreview() : ''
const cpyDocNumber = cpyType ? generateCpyPreview() : ''
- // 선택된 Document Class의 code 값을 가져오기 (SWP API의 DOC_CLASS로 사용)
+ // 선택된 Document Class의 value 값을 가져오기 (SWP API의 DOC_CLASS로 사용)
const selectedDocClass = documentClasses.find(cls => String(cls.id) === data.documentClassId)
- const docClassCode = selectedDocClass?.code || ''
+ const docClassValue = selectedDocClass?.value || ''
// documentNumberTypeId는 SHI를 우선 사용, 없으면 CPY 사용
const documentNumberTypeId = shiType?.id || cpyType?.id
@@ -391,7 +391,7 @@ export function AddDocumentDialog({
contractId,
documentNumberTypeId,
documentClassId: Number(data.documentClassId),
- docClass: docClassCode, // 첫 번째 선택기의 code 값 사용 (A, B, C 등)
+ docClass: docClassValue, // Document Class의 value 값 사용 (A Class, B Class 등)
title: data.title,
docNumber: shiDocNumber,
vendorDocNumber: cpyDocNumber,
@@ -610,8 +610,10 @@ export function AddDocumentDialog({
<SelectContent>
{documentClasses.map((cls) => (
<SelectItem key={cls.id} value={String(cls.id)}>
- <span className="font-mono font-semibold">{cls.code}</span>
- {cls.description && <span className="text-muted-foreground"> - {cls.description}</span>}
+ <span className="font-mono font-bold">{cls.value}</span>
+ {cls.description && cls.description.trim() !== '' && (
+ <span className="text-muted-foreground"> - {cls.description}</span>
+ )}
</SelectItem>
))}
</SelectContent>
diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts
index 30f57a23..1a7b1ab6 100644
--- a/lib/vendor-document-list/plant/document-stages-service.ts
+++ b/lib/vendor-document-list/plant/document-stages-service.ts
@@ -804,7 +804,8 @@ export async function getDocumentClassOptionsByContract(contractId: number) {
.select({
id: documentClasses.id,
code: documentClasses.code,
- description: documentClasses.value,
+ value: documentClasses.value,
+ description: documentClasses.description,
})
.from(documentClasses)
.where(
@@ -1374,18 +1375,18 @@ export async function uploadImportData(data: UploadData) {
}
- // 2. Document Class 매핑 가져오기 (트랜잭션 밖에서) - code 기반으로 통일
+ // 2. Document Class 매핑 가져오기 (트랜잭션 밖에서) - value 기반으로 통일
const documentClassesData = await db
.select({
id: documentClasses.id,
- code: documentClasses.code,
+ value: documentClasses.value,
description: documentClasses.description,
})
.from(documentClasses)
.where(and(eq(documentClasses.projectId, contract.projectId), eq(documentClasses.isActive, true)))
const classMap = new Map(
- documentClassesData.map(dc => [dc.code, dc.id])
+ documentClassesData.map(dc => [dc.value, dc.id])
)
console.log(classMap)
diff --git a/lib/vendor-document-list/plant/excel-import-stage.tsx b/lib/vendor-document-list/plant/excel-import-stage.tsx
index 9bd9ed4c..24db6ea1 100644
--- a/lib/vendor-document-list/plant/excel-import-stage.tsx
+++ b/lib/vendor-document-list/plant/excel-import-stage.tsx
@@ -421,7 +421,7 @@ async function createImportTemplate(projectType: "ship" | "plant", contractId: n
const res = await getDocumentClassOptionsByContract(contractId)
if (!res.success) throw new Error(res.error || "데이터 로딩 실패")
- const documentClasses = res.data.classes as Array<{ id: number; code: string; description: string }>
+ const documentClasses = res.data.classes as Array<{ id: number; value: string; description: string }>
const options = res.data.options as Array<{ documentClassId: number; optionValue: string }>
// 클래스별 옵션 맵
@@ -460,7 +460,7 @@ async function createImportTemplate(projectType: "ship" | "plant", contractId: n
const sampleRow = [
projectType === "ship" ? "SH-2024-001" : "PL-2024-001",
"샘플 문서명",
- firstClass ? firstClass.code : "",
+ firstClass ? firstClass.value : "", // value 사용 (A Class, B Class 등)
...(projectType === "plant" ? ["V-001"] : []),
...allStageNames.map((s) => (firstClassStages.includes(s) ? "2024-03-01" : "")),
]
@@ -648,8 +648,8 @@ if (projectType === "plant") {
styleHeaderRow(matrixHeaderRow, "FF34495E")
for (const docClass of documentClasses) {
const validStages = new Set(optionsByClassId.get(docClass.id) ?? [])
- // Code를 사용하고 설명을 괄호 안에 추가
- const row = [`${docClass.code}`, ...allStageNames.map((stage) => (validStages.has(stage) ? "✓" : ""))]
+ // Value를 사용 (A Class, B Class 등)
+ const row = [`${docClass.value}`, ...allStageNames.map((stage) => (validStages.has(stage) ? "✓" : ""))]
const dataRow = matrixSheet.addRow(row)
allStageNames.forEach((stage, idx) => {
const cell = dataRow.getCell(idx + 2)
@@ -712,8 +712,8 @@ if (projectType === "plant") {
const referenceSheet = workbook.addWorksheet("ReferenceData", { state: "hidden" })
referenceSheet.getCell("A1").value = "DocumentClasses"
documentClasses.forEach((dc, idx) => {
- // 코드를 메인으로, 설명을 참고용으로 표시
- referenceSheet.getCell(`A${idx + 2}`).value = dc.code
+ // value를 메인으로 (A Class, B Class 등), 설명을 참고용으로 표시
+ referenceSheet.getCell(`A${idx + 2}`).value = dc.value
referenceSheet.getCell(`B${idx + 2}`).value = dc.description
})