diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-13 12:21:18 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-13 12:21:18 +0900 |
| commit | d113d1732f7c6356af6619dfaff98604fb68e5ad (patch) | |
| tree | ea77e9c4a5da0123e9aa5e1482d9f1c93afe76ba /lib | |
| parent | a4ceade24d28af0bde985bf750017efc02f053ff (diff) | |
(김준회) SWP 문서리스트: OWN_DOC_NO로 스테이지 검증 추가
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/swp/api-client.ts | 1 | ||||
| -rw-r--r-- | lib/swp/swp-upload-server-actions.ts | 63 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-toolbar.tsx | 90 | ||||
| -rw-r--r-- | lib/swp/table/swp-upload-validation-dialog.tsx | 69 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/document-stages-service.ts | 104 |
5 files changed, 269 insertions, 58 deletions
diff --git a/lib/swp/api-client.ts b/lib/swp/api-client.ts index 1befa217..39ce02b0 100644 --- a/lib/swp/api-client.ts +++ b/lib/swp/api-client.ts @@ -100,6 +100,7 @@ export interface SwpDocumentApiResponse { // 선택적 필드 (null 가능) DOC_GB: string | null; DOC_TYPE: string | null; + DOC_CLS: string | null; // Document Class (A, B, C 등) OWN_DOC_NO: string | null; SHI_DOC_NO: string | null; PROJ_NM: string | null; diff --git a/lib/swp/swp-upload-server-actions.ts b/lib/swp/swp-upload-server-actions.ts new file mode 100644 index 00000000..2c07cf77 --- /dev/null +++ b/lib/swp/swp-upload-server-actions.ts @@ -0,0 +1,63 @@ +"use server"; + +/** + * SWP 파일 업로드 관련 서버 액션 + * 클라이언트 컴포넌트에서 호출할 수 있도록 래핑 + */ + +import { getProjectIdByCode } from "./project-utils"; +import { getDocumentClassInfoForSwpUpload } from "@/lib/vendor-document-list/plant/document-stages-service"; + +/** + * 프로젝트 코드로 Document Class 정보 조회 + * @param projectCode 프로젝트 코드 (PROJ_NO) + * @returns vendorDocNumber → Document Class 매핑 + Document Class → 허용 Stage 목록 + */ +export async function getDocumentClassInfoByProjectCode(projectCode: string): Promise<{ + success: boolean; + vendorDocNumberToDocClassMap: Record<string, string>; + documentClassStages: Record<string, string[]>; + error?: string; +}> { + try { + console.log(`[getDocumentClassInfoByProjectCode] 프로젝트 ${projectCode} 정보 조회 시작`); + + // 1. 프로젝트 코드 → 프로젝트 ID 변환 + const projectId = await getProjectIdByCode(projectCode); + + if (!projectId) { + console.warn(`[getDocumentClassInfoByProjectCode] 프로젝트 ID를 찾을 수 없음: ${projectCode}`); + return { + success: false, + vendorDocNumberToDocClassMap: {}, + documentClassStages: {}, + error: `프로젝트를 찾을 수 없습니다: ${projectCode}`, + }; + } + + console.log(`[getDocumentClassInfoByProjectCode] 프로젝트 ID: ${projectId}`); + + // 2. EVCP DB에서 문서 정보 조회 + const classInfo = await getDocumentClassInfoForSwpUpload(projectId); + + console.log(`[getDocumentClassInfoByProjectCode] 조회 완료:`, { + vendorDocNumbers: Object.keys(classInfo.vendorDocNumberToDocClassMap).length, + documentClasses: Object.keys(classInfo.documentClassStages).length, + }); + + return { + success: true, + vendorDocNumberToDocClassMap: classInfo.vendorDocNumberToDocClassMap, + documentClassStages: classInfo.documentClassStages, + }; + } catch (error) { + console.error("[getDocumentClassInfoByProjectCode] 오류:", error); + return { + success: false, + vendorDocNumberToDocClassMap: {}, + documentClassStages: {}, + error: error instanceof Error ? error.message : "문서 정보 조회 실패", + }; + } +} + diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx index 013b4a13..ea5ee729 100644 --- a/lib/swp/table/swp-table-toolbar.tsx +++ b/lib/swp/table/swp-table-toolbar.tsx @@ -19,7 +19,7 @@ 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 { getDocumentClassInfoByProjectCode } from "@/lib/swp/swp-upload-server-actions"; import type { DocumentListItem } from "@/lib/swp/document-service"; interface SwpTableFilters { @@ -40,7 +40,7 @@ interface SwpTableToolbarProps { vendorCode?: string; droppedFiles?: File[]; onFilesProcessed?: () => void; - documents?: DocumentListItem[]; // 업로드 권한 검증 + DOC_TYPE 확인용 문서 목록 + documents?: DocumentListItem[]; // 업로드 권한 검증 + DOC_CLS (Document Class) 확인용 문서 목록 userId?: string; // 파일 취소 시 필요 } @@ -82,31 +82,25 @@ export function SwpTableToolbar({ }>>([]); const [showValidationDialog, setShowValidationDialog] = useState(false); - // Document Class-Stage 매핑 (프로젝트별) + // EVCP DB에서 조회한 문서 정보 (vendorDocNumber → Document Class 매핑) + const [vendorDocNumberToDocClassMap, setVendorDocNumberToDocClassMap] = useState<Record<string, string>>({}); + // Document Class별 허용 Stage 목록 const [documentClassStages, setDocumentClassStages] = useState<Record<string, string[]>>({}); - const [isLoadingDocClassStages, setIsLoadingDocClassStages] = useState(false); /** * 업로드 가능한 문서번호 목록 추출 (OWN_DOC_NO 기준) + * SWP API의 OWN_DOC_NO가 EVCP DB의 vendorDocNumber와 매핑되는지 확인 */ const availableDocNos = useMemo(() => { return documents .map(doc => doc.OWN_DOC_NO) - .filter((ownDocNo): ownDocNo is string => ownDocNo !== null && ownDocNo !== undefined); - }, [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]); + .filter((ownDocNo): ownDocNo is string => { + // OWN_DOC_NO가 있고, EVCP DB에 등록된 문서인지 확인 + return ownDocNo !== null && + ownDocNo !== undefined && + vendorDocNumberToDocClassMap[ownDocNo] !== undefined; + }); + }, [documents, vendorDocNumberToDocClassMap]); /** * 벤더 모드 여부 (벤더 코드가 있으면 벤더 모드) @@ -114,42 +108,68 @@ export function SwpTableToolbar({ const isVendorMode = !!vendorCode; /** - * 프로젝트 변경 시 Document Class-Stage 매핑 로드 + * 프로젝트 변경 시 EVCP DB에서 문서 정보 로드 + * - vendorDocNumber → docClass 매핑 + * - Document Class별 허용 Stage 목록 */ useEffect(() => { if (!projNo) { + setVendorDocNumberToDocClassMap({}); setDocumentClassStages({}); return; } let isCancelled = false; - const loadDocumentClassStages = async () => { + const loadDocumentClassInfo = async () => { try { - setIsLoadingDocClassStages(true); - const stages = await getProjectDocumentClassStages(projNo); + console.log(`[SwpTableToolbar] 프로젝트 ${projNo} 문서 정보 로드 시작`); + + // 서버 액션 호출 + const result = await getDocumentClassInfoByProjectCode(projNo); + if (!isCancelled) { - setDocumentClassStages(stages); - console.log(`[SwpTableToolbar] Document Class-Stage 매핑 로드 완료:`, stages); + if (result.success) { + setVendorDocNumberToDocClassMap(result.vendorDocNumberToDocClassMap); + setDocumentClassStages(result.documentClassStages); + + console.log(`[SwpTableToolbar] 문서 정보 로드 완료:`, { + vendorDocNumbers: Object.keys(result.vendorDocNumberToDocClassMap).length, + documentClassStages: result.documentClassStages, + }); + } else { + console.warn(`[SwpTableToolbar] 문서 정보 로드 실패:`, result.error); + setVendorDocNumberToDocClassMap({}); + setDocumentClassStages({}); + + toast({ + variant: "destructive", + title: "문서 정보 로드 실패", + description: result.error || "문서 정보를 가져올 수 없습니다.", + }); + } } } catch (error) { if (!isCancelled) { - console.error('[SwpTableToolbar] Document Class-Stage 매핑 로드 실패:', error); + console.error('[SwpTableToolbar] 문서 정보 로드 실패:', error); + setVendorDocNumberToDocClassMap({}); setDocumentClassStages({}); - } - } finally { - if (!isCancelled) { - setIsLoadingDocClassStages(false); + + toast({ + variant: "destructive", + title: "문서 정보 로드 실패", + description: "문서 정보를 가져올 수 없습니다. 페이지를 새로고침해주세요.", + }); } } }; - loadDocumentClassStages(); + loadDocumentClassInfo(); return () => { isCancelled = true; }; - }, [projNo]); + }, [projNo, toast]); /** * 드롭된 파일 처리 - useEffect로 감지하여 자동 검증 @@ -183,7 +203,7 @@ export function SwpTableToolbar({ file.name, availableDocNos, isVendorMode, - docNoToDocTypeMap, + vendorDocNumberToDocClassMap, documentClassStages ); return { @@ -198,7 +218,7 @@ export function SwpTableToolbar({ setShowValidationDialog(true); onFilesProcessed?.(); } - }, [droppedFiles, projNo, vendorCode, toast, onFilesProcessed, availableDocNos, isVendorMode, docNoToDocTypeMap, documentClassStages]); + }, [droppedFiles, projNo, vendorCode, toast, onFilesProcessed, availableDocNos, isVendorMode, vendorDocNumberToDocClassMap, documentClassStages]); /** * 파일 업로드 핸들러 @@ -240,7 +260,7 @@ export function SwpTableToolbar({ file.name, availableDocNos, isVendorMode, - docNoToDocTypeMap, + vendorDocNumberToDocClassMap, documentClassStages ); return { diff --git a/lib/swp/table/swp-upload-validation-dialog.tsx b/lib/swp/table/swp-upload-validation-dialog.tsx index ef48f0c6..a7cdb7c5 100644 --- a/lib/swp/table/swp-upload-validation-dialog.tsx +++ b/lib/swp/table/swp-upload-validation-dialog.tsx @@ -43,14 +43,14 @@ interface SwpUploadValidationDialogProps { * @param fileName 검증할 파일명 * @param availableDocNos 업로드 가능한 문서번호 목록 (선택) * @param isVendorMode 벤더 모드인지 여부 (true인 경우 문서번호 검증 필수) - * @param docNoToDocTypeMap 문서번호 → DOC_TYPE 매핑 (Stage 검증용) + * @param docNoToDocClsMap 문서번호 → DOC_CLS (Document Class) 매핑 (Stage 검증용) * @param documentClassStages Document Class → 허용 Stage 목록 매핑 */ export function validateFileName( fileName: string, availableDocNos?: string[], isVendorMode?: boolean, - docNoToDocTypeMap?: Record<string, string>, + docNoToDocClsMap?: Record<string, string>, documentClassStages?: Record<string, string[]> ): { valid: boolean; @@ -117,10 +117,12 @@ export function validateFileName( }; } + // trim된 값 미리 준비 (중복 제거) + const trimmedDocNo = ownDocNo.trim(); + const trimmedStage = stage.trim(); + // 문서번호 검증 (벤더 모드에서는 필수) if (isVendorMode) { - const trimmedDocNo = ownDocNo.trim(); - // 벤더 모드에서 문서 목록이 비어있으면 에러 if (!availableDocNos || availableDocNos.length === 0) { return { @@ -138,28 +140,49 @@ export function validateFileName( } } - // Stage 검증 (DOC_TYPE별 허용 Stage 확인) - const trimmedDocNo = ownDocNo.trim(); - const trimmedStage = stage.trim(); + // Stage 검증 (Document Class별 허용 Stage 확인) + // EVCP DB에서 vendorDocNumber로 Document Class를 조회하고, + // 해당 Document Class의 허용 Stage 목록과 비교 + + if (docNoToDocClsMap && documentClassStages) { + const docCls = docNoToDocClsMap[trimmedDocNo]; + console.log(`[validateFileName] 문서 '${trimmedDocNo}' → Document Class: '${docCls || "null"}'`); + + if (!docCls) { + // 문서가 EVCP DB에 등록되지 않음 + return { + valid: false, + error: `문서번호 '${trimmedDocNo}'는 문서 리스트에 등록되지 않았습니다. 먼저 문서 리스트를 제출해주세요.`, + }; + } - if (docNoToDocTypeMap && documentClassStages) { - const docType = docNoToDocTypeMap[trimmedDocNo]; + const allowedStages = documentClassStages[docCls]; + console.log(`[validateFileName] Document Class '${docCls}' → 허용 Stage:`, allowedStages); - 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 검증을 스킵 (설정되지 않은 경우) + if (!allowedStages || allowedStages.length === 0) { + // Document Class에 Stage가 설정되지 않음 + return { + valid: false, + error: `문서 '${trimmedDocNo}'의 Document Class '${docCls}'에 Stage가 설정되지 않았습니다. 관리자에게 문의하세요.`, + }; + } + + console.log(`[validateFileName] Stage 검증: '${trimmedStage}' in [${allowedStages.join(", ")}]`); + if (!allowedStages.includes(trimmedStage)) { + return { + valid: false, + error: `문서 '${trimmedDocNo}'의 Document Class '${docCls}'에서 Stage '${trimmedStage}'는 허용되지 않습니다. 허용된 Stage: ${allowedStages.join(", ")}`, + }; } - // docType이 없으면 Stage 검증을 스킵 (문서 정보가 없는 경우) + + console.log(`[validateFileName] Stage 검증 통과: '${trimmedStage}'`); + } else { + // 검증 정보가 로드되지 않음 + console.log(`[validateFileName] 검증 정보가 없음 → 업로드 차단`); + return { + valid: false, + error: "문서 정보를 가져올 수 없습니다. 페이지를 새로고침하거나 프로젝트를 다시 선택해주세요.", + }; } return { diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts index 7740a385..ed4099b3 100644 --- a/lib/vendor-document-list/plant/document-stages-service.ts +++ b/lib/vendor-document-list/plant/document-stages-service.ts @@ -1560,3 +1560,107 @@ export async function uploadImportData(data: UploadData) { } } } + +/** + * SWP 파일 업로드 검증을 위한 문서 정보 조회 + * - vendorDocNumber → docClass 매핑 + * - Document Class별 허용 Stage 목록 + * + * @param projectId 프로젝트 ID + * @returns vendorDocNumber → Document Class 매핑 + Document Class → 허용 Stage 목록 + */ +export async function getDocumentClassInfoForSwpUpload(projectId: number): Promise<{ + vendorDocNumberToDocClassMap: Record<string, string>; + documentClassStages: Record<string, string[]>; +}> { + try { + console.log(`[getDocumentClassInfoForSwpUpload] 프로젝트 ${projectId} 문서 정보 조회 시작`); + + // 1. 프로젝트의 모든 문서와 해당 docClass 조회 + const documents = await db + .select({ + vendorDocNumber: stageDocuments.vendorDocNumber, + docClass: stageDocuments.docClass, + }) + .from(stageDocuments) + .where( + and( + eq(stageDocuments.projectId, projectId), + eq(stageDocuments.status, "ACTIVE") + ) + ); + + console.log(`[getDocumentClassInfoForSwpUpload] 조회된 문서 개수: ${documents.length}`); + + // 2. vendorDocNumber → docClass 매핑 생성 + const vendorDocNumberToDocClassMap: Record<string, string> = {}; + const documentClassSet = new Set<string>(); + + for (const doc of documents) { + if (doc.vendorDocNumber && doc.docClass) { + vendorDocNumberToDocClassMap[doc.vendorDocNumber] = doc.docClass; + documentClassSet.add(doc.docClass); + } + } + + console.log(`[getDocumentClassInfoForSwpUpload] vendorDocNumber 매핑 개수: ${Object.keys(vendorDocNumberToDocClassMap).length}`); + console.log(`[getDocumentClassInfoForSwpUpload] 사용 중인 Document Class: ${Array.from(documentClassSet).join(", ")}`); + + // 3. 프로젝트의 Document Class별 허용 Stage 목록 조회 + if (documentClassSet.size === 0) { + console.log(`[getDocumentClassInfoForSwpUpload] Document Class가 없음 → 빈 결과 반환`); + return { + vendorDocNumberToDocClassMap: {}, + documentClassStages: {}, + }; + } + + // 프로젝트의 모든 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); + + console.log(`[getDocumentClassInfoForSwpUpload] Document Class 옵션 조회 개수: ${documentClassesWithOptions.length}`); + + // 4. Document Class별로 Stage 목록 그룹핑 + const documentClassStages: Record<string, string[]> = {}; + + for (const row of documentClassesWithOptions) { + if (!row.docClassValue || !row.optionCode) continue; + + if (!documentClassStages[row.docClassValue]) { + documentClassStages[row.docClassValue] = []; + } + + documentClassStages[row.docClassValue].push(row.optionCode); + } + + console.log(`[getDocumentClassInfoForSwpUpload] Document Class-Stage 매핑:`, documentClassStages); + + return { + vendorDocNumberToDocClassMap, + documentClassStages, + }; + } catch (error) { + console.error('[getDocumentClassInfoForSwpUpload] 오류:', error); + return { + vendorDocNumberToDocClassMap: {}, + documentClassStages: {}, + }; + } +} |
