From 969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 14 Aug 2025 11:54:47 +0000 Subject: (대표님) 정규벤더등록, 벤더문서관리, 벤더데이터입력, 첨부파일관리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/sedp/get-form-tags.ts | 9 +- lib/tags/table/add-tag-dialog.tsx | 76 +-- lib/vendor-document-list/dolce-upload-service.ts | 606 +++++++++++---------- .../enhanced-document-service.ts | 163 +++++- .../plant/document-stages-table.tsx | 39 +- .../ship/send-to-shi-button.tsx | 18 +- lib/vendor-document-list/sync-service.ts | 65 ++- .../table/enhanced-documents-table.tsx | 90 +-- .../vendor-registration-status-view.tsx | 260 ++------- .../major-items-update-sheet.tsx | 245 +++++++++ lib/vendor-regular-registrations/repository.ts | 157 +++++- .../safety-qualification-update-sheet.tsx | 143 +++++ lib/vendor-regular-registrations/service.ts | 400 +++++++++++++- .../table/safety-qualification-update-sheet.tsx | 143 +++++ .../vendor-regular-registrations-table-columns.tsx | 105 +++- ...regular-registrations-table-toolbar-actions.tsx | 108 ++-- lib/vendors/table/request-pq-dialog.tsx | 34 +- 17 files changed, 1921 insertions(+), 740 deletions(-) create mode 100644 lib/vendor-regular-registrations/major-items-update-sheet.tsx create mode 100644 lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx create mode 100644 lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx (limited to 'lib') diff --git a/lib/sedp/get-form-tags.ts b/lib/sedp/get-form-tags.ts index efa4a9c0..34f990f3 100644 --- a/lib/sedp/get-form-tags.ts +++ b/lib/sedp/get-form-tags.ts @@ -459,6 +459,8 @@ export async function importTagsFromSEDP( } } + const packageCode = projectType === "ship" ? tagEntry.ATTRIBUTES.find(v=>v.ATT_ID === "CM3003")?.VALUE :tagEntry.ATTRIBUTES.find(v=>v.ATT_ID === "ME5074")?.VALUE + // 기본 태그 데이터 객체 생성 (formEntries용) const tagObject: any = { TAG_IDX: tagEntry.TAG_IDX, // SEDP 고유 식별자 @@ -468,7 +470,7 @@ export async function importTagsFromSEDP( VNDRCD: vendorRecord[0].vendorCode, VNDRNM_1: vendorRecord[0].vendorName, status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시 - ...(projectType === "ship" ? { CM3003: tagEntry.CM3003 } : { ME5074: tagEntry.ME5074 }) + ...(projectType === "ship" ? { CM3003: packageCode } : { ME5074:packageCode }) } // tags 테이블용 데이터 (UPSERT용) @@ -542,6 +544,11 @@ export async function importTagsFromSEDP( hasUpdates = true; continue; } + if (key === "CLS_ID" && tagObject[key] !== existingTag.data[key]) { + updates[key] = tagObject[key]; + hasUpdates = true; + continue; + } const columnInfo = columnsJSON.find(col => col.key === key); if (columnInfo && columnInfo.shi === true) { diff --git a/lib/tags/table/add-tag-dialog.tsx b/lib/tags/table/add-tag-dialog.tsx index e5207cd8..f3eaed3f 100644 --- a/lib/tags/table/add-tag-dialog.tsx +++ b/lib/tags/table/add-tag-dialog.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { useRouter } from "next/navigation" +import { useRouter, useParams } from "next/navigation" import { useForm, useWatch, useFieldArray } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { toast } from "sonner" @@ -51,6 +51,7 @@ import { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import { Badge } from "@/components/ui/badge" +import { useTranslation } from "@/i18n/client" import type { CreateTagSchema } from "@/lib/tags/validations" import { createTagSchema } from "@/lib/tags/validations" @@ -102,6 +103,9 @@ interface AddTagDialogProps { export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const router = useRouter() + const params = useParams() + const lng = (params?.lng as string) || "ko" + const { t } = useTranslation(lng, "engineering") const [open, setOpen] = React.useState(false) const [tagTypeList, setTagTypeList] = React.useState([]) @@ -134,7 +138,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { - toast.error("클래스 옵션을 불러오는데 실패했습니다.") + toast.error(t("toast.classOptionsLoadFailed")) } finally { setIsLoadingClasses(false) } @@ -198,7 +202,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { form.setValue("rows", updatedRows); return true } catch (err) { - toast.error("서브필드를 불러오는데 실패했습니다.") + toast.error(t("toast.subfieldsLoadFailed")) setSubFields([]) return false } finally { @@ -310,7 +314,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { // --------------- async function onSubmit(data: MultiTagFormValues) { if (!selectedPackageId) { - toast.error("No selectedPackageId."); + toast.error(t("toast.noSelectedPackageId")); return; } @@ -353,12 +357,12 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { // Show results to the user if (successfulTags.length > 0) { - toast.success(`${successfulTags.length}개의 태그가 성공적으로 생성되었습니다!`); + toast.success(`${successfulTags.length}${t("toast.tagsCreatedSuccess")}`); } if (failedTags.length > 0) { console.log("Failed tags:", failedTags); - toast.error(`${failedTags.length}개의 태그 생성에 실패했습니다.`); + toast.error(`${failedTags.length}${t("toast.tagsCreateFailed")}`); } // Refresh the page @@ -370,7 +374,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { setOpen(false); } } catch (err) { - toast.error("태그 생성 처리에 실패했습니다."); + toast.error(t("toast.tagProcessingFailed")); } finally { setIsSubmitting(false); } @@ -435,7 +439,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { return ( - Class + {t("labels.class")} @@ -448,13 +452,13 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { > {isLoadingClasses ? ( <> - 클래스 로딩 중... + {t("messages.loadingClasses")} ) : ( <> - {field.value || "클래스 선택..."} + {field.value || t("placeholders.selectClass")} @@ -465,7 +469,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { @@ -475,7 +479,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }}> - 검색 결과가 없습니다. + {t("messages.noSearchResults")} {classOptions.map((opt, optIndex) => { if (!classOptionIdsRef.current[opt.code]) { @@ -543,7 +547,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { disabled={!selectedClassOption} > - + {selectedClassOption?.subclasses.map((subclass) => ( @@ -576,7 +580,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { return ( - Tag Type + {t("labels.tagType")} {isReadOnly ? (
@@ -592,7 +596,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { key={`tag-type-placeholder-${inputId}`} {...field} readOnly - placeholder="클래스 선택시 자동으로 결정됩니다" + placeholder={t("placeholders.autoSetByClass")} className="h-9 bg-muted" /> )} @@ -610,15 +614,15 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { return (
-
필드 로딩 중...
+
{t("messages.loadingFields")}
) } if (subFields.length === 0 && selectedTagTypeCode) { const message = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 - ? "서브클래스를 선택해주세요." - : "이 태그 유형에 대한 필드가 없습니다." + ? t("messages.selectSubclassFirst") + : t("messages.noFieldsForTagType") return (
@@ -630,7 +634,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { if (subFields.length === 0) { return (
- 태그 데이터를 입력하려면 먼저 상단에서 클래스를 선택하세요. + {t("messages.selectClassFirst")}
) } @@ -639,10 +643,10 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
{/* 헤더 */}
-

태그 항목 ({fields.length}개)

+

{t("sections.tagItems")} ({fields.length}개)

{!areAllTagNosValid && ( - 유효하지 않은 태그 존재 + {t("messages.invalidTagsExist")} )}
@@ -655,10 +659,10 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { # -
Tag No
+
{t("labels.tagNo")}
-
Description
+
{t("labels.description")}
{/* Subfields */} @@ -680,7 +684,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { ))} - Actions + {t("labels.actions")}
@@ -738,7 +742,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { @@ -768,7 +772,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { className="w-full h-8 truncate" title={field.value || ""} > - + )} @@ -820,7 +824,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { -

행 복제

+

{t("tooltips.duplicateRow")}

@@ -843,7 +847,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { -

행 삭제

+

{t("tooltips.deleteRow")}

@@ -864,7 +868,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { disabled={!selectedTagTypeCode || isLoadingSubFields || subFields.length === 0} > - 새 행 추가 + {t("buttons.addRow")}
@@ -903,15 +907,15 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { > - 새 태그 추가 + {t("dialogs.addFormTag")} - 클래스와 서브클래스를 선택하여 태그 유형과 하위 필드를 로드한 다음, 여러 행을 추가하여 여러 태그를 생성하세요. + {t("dialogs.selectClassToLoadFields")} @@ -968,7 +972,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { }} disabled={isSubmitting} > - 취소 + {t("buttons.cancel")}
diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index 2d6a83c6..84ae4525 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -5,6 +5,8 @@ import { eq, and, desc, sql, inArray, min } from "drizzle-orm" import { v4 as uuidv4 } from "uuid" import path from "path" import * as crypto from "crypto" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" export interface DOLCEUploadResult { success: boolean @@ -87,7 +89,7 @@ interface DOLCEFileMapping { function getFileReaderConfig(): FileReaderConfig { const isProduction = process.env.NODE_ENV === "production"; - + if (isProduction) { return { baseDir: process.env.NAS_PATH || "/evcp_nas", // NAS 기본 경로 @@ -118,13 +120,13 @@ class DOLCEUploadService { ): Promise { try { console.log(`Starting DOLCE upload for contract ${projectId}, revisions: ${revisionIds.join(', ')}`) - + // 1. 계약 정보 조회 (프로젝트 코드, 벤더 코드 등) const contractInfo = await this.getContractInfo(projectId) if (!contractInfo) { throw new Error(`Contract info not found for ID: ${projectId}`) } - + // 2. 업로드할 리비전 정보 조회 const revisionsToUpload = await this.getRevisionsForUpload(revisionIds) if (revisionsToUpload.length === 0) { @@ -134,7 +136,7 @@ class DOLCEUploadService { uploadedFiles: 0 } } - + let uploadedDocuments = 0 let uploadedFiles = 0 const errors: string[] = [] @@ -143,19 +145,19 @@ class DOLCEUploadService { fileResults: [], mappingResults: [] } - + // 3. 각 리비전별로 처리 for (const revision of revisionsToUpload) { try { console.log(`Processing revision ${revision.revision} for document ${revision.documentNo}`) - + // 3-1. UploadId 미리 생성 (파일이 있는 경우에만) let uploadId: string | undefined if (revision.attachments && revision.attachments.length > 0) { uploadId = uuidv4() // 문서 업로드 시 사용할 UploadId 미리 생성 console.log(`Generated UploadId for document upload: ${uploadId}`) } - + // 3-2. 문서 정보 업로드 (UploadId 포함) const dolceDoc = this.transformToDoLCEDocument( revision, @@ -163,43 +165,43 @@ class DOLCEUploadService { uploadId, // 미리 생성된 UploadId 사용 contractInfo.vendorCode, ) - + const docResult = await this.uploadDocument([dolceDoc], userId) if (!docResult.success) { errors.push(`Document upload failed for ${revision.documentNo}: ${docResult.error}`) continue // 문서 업로드 실패 시 다음 리비전으로 넘어감 } - + uploadedDocuments++ results.documentResults.push(docResult) console.log(`✅ Document uploaded successfully: ${revision.documentNo}`) - + // 3-3. 파일 업로드 (이미 생성된 UploadId 사용) if (uploadId && revision.attachments && revision.attachments.length > 0) { try { // 파일 업로드 시 이미 생성된 UploadId 사용 const fileUploadResults = await this.uploadFiles( - revision.attachments, - userId, + revision.attachments, + userId, uploadId // 이미 생성된 UploadId 전달 ) - + } catch (fileError) { errors.push(`File upload failed for ${revision.documentNo}: ${fileError instanceof Error ? fileError.message : 'Unknown error'}`) console.error(`❌ File upload failed for ${revision.documentNo}:`, fileError) } } - + // 3-5. 성공한 리비전의 상태 업데이트 await this.updateRevisionStatus(revision.id, 'SUBMITTED', uploadId) - + } catch (error) { const errorMessage = `Failed to process revision ${revision.revision}: ${error instanceof Error ? error.message : 'Unknown error'}` errors.push(errorMessage) console.error(errorMessage, error) } } - + return { success: errors.length === 0, uploadedDocuments, @@ -207,7 +209,7 @@ class DOLCEUploadService { errors: errors.length > 0 ? errors : undefined, results } - + } catch (error) { console.error('DOLCE upload failed:', error) throw error @@ -216,22 +218,34 @@ class DOLCEUploadService { /** * 계약 정보 조회 */ - private async getContractInfo(projectId: number) { + private async getContractInfo(projectId: number): Promise<{ + projectCode: string; + vendorCode: string; + } | null> { + + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + throw new Error("인증이 필요합니다.") + } + + const [result] = await db .select({ projectCode: projects.code, - vendorCode: vendors.vendorCode, - contractNo: contracts.contractNo + vendorCode: vendors.vendorCode }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) - .where(eq(contracts.projectId, projectId)) + .where(and(eq(contracts.projectId, projectId), eq(contracts.vendorId, Number(session.user.companyId)))) .limit(1) - return result + return result?.projectCode && result?.vendorCode + ? { projectCode: result.projectCode, vendorCode: result.vendorCode } + : null } + /** * 각 issueStageId별로 첫 번째 revision 정보를 조회 */ @@ -264,7 +278,7 @@ class DOLCEUploadService { .select({ // revision 테이블 정보 id: revisions.id, - registerId:revisions.registerId, + registerId: revisions.registerId, revision: revisions.revision, // revisionNo가 아니라 revision revisionStatus: revisions.revisionStatus, uploaderId: revisions.uploaderId, @@ -341,181 +355,181 @@ class DOLCEUploadService { return revisionsWithAttachments } -/** - * 파일 업로드 (PWPUploadService.ashx) - 수정된 버전 - * @param attachments 업로드할 첨부파일 목록 - * @param userId 사용자 ID - * @param uploadId 이미 생성된 UploadId (문서 업로드 시 생성됨) - */ -private async uploadFiles( - attachments: any[], - userId: string, - uploadId: string // 이미 생성된 UploadId를 매개변수로 받음 -): Promise> { - const uploadResults = [] - const resultDataArray: ResultData[] = [] + /** + * 파일 업로드 (PWPUploadService.ashx) - 수정된 버전 + * @param attachments 업로드할 첨부파일 목록 + * @param userId 사용자 ID + * @param uploadId 이미 생성된 UploadId (문서 업로드 시 생성됨) + */ + private async uploadFiles( + attachments: any[], + userId: string, + uploadId: string // 이미 생성된 UploadId를 매개변수로 받음 + ): Promise> { + const uploadResults = [] + const resultDataArray: ResultData[] = [] - for (let i = 0; i < attachments.length; i++) { - const attachment = attachments[i] - try { - // FileId만 새로 생성 (UploadId는 이미 생성된 것 사용) - const fileId = uuidv4() + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i] + try { + // FileId만 새로 생성 (UploadId는 이미 생성된 것 사용) + const fileId = uuidv4() - console.log(`Uploading file with predefined UploadId: ${uploadId}, FileId: ${fileId}`) + console.log(`Uploading file with predefined UploadId: ${uploadId}, FileId: ${fileId}`) - // 파일 데이터 읽기 - const fileBuffer = await this.getFileBuffer(attachment.filePath) + // 파일 데이터 읽기 + const fileBuffer = await this.getFileBuffer(attachment.filePath) - const uploadUrl = `${this.UPLOAD_SERVICE_URL}?UploadId=${uploadId}&FileId=${fileId}` + const uploadUrl = `${this.UPLOAD_SERVICE_URL}?UploadId=${uploadId}&FileId=${fileId}` - const response = await fetch(uploadUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream', - }, - body: fileBuffer - }) + const response = await fetch(uploadUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + }, + body: fileBuffer + }) - if (!response.ok) { - const errorText = await response.text() - throw new Error(`File upload failed: HTTP ${response.status} - ${errorText}`) - } + if (!response.ok) { + const errorText = await response.text() + throw new Error(`File upload failed: HTTP ${response.status} - ${errorText}`) + } - const dolceFilePath = await response.text() // DOLCE에서 반환하는 파일 경로 - - // 업로드 성공 후 documentAttachments 테이블 업데이트 - await db - .update(documentAttachments) - .set({ - uploadId: uploadId, // 이미 생성된 UploadId 사용 - fileId: fileId, - uploadedBy: userId, - dolceFilePath: dolceFilePath, - uploadedAt: new Date(), - updatedAt: new Date() + const dolceFilePath = await response.text() // DOLCE에서 반환하는 파일 경로 + + // 업로드 성공 후 documentAttachments 테이블 업데이트 + await db + .update(documentAttachments) + .set({ + uploadId: uploadId, // 이미 생성된 UploadId 사용 + fileId: fileId, + uploadedBy: userId, + dolceFilePath: dolceFilePath, + uploadedAt: new Date(), + updatedAt: new Date() + }) + .where(eq(documentAttachments.id, attachment.id)) + + uploadResults.push({ + uploadId, + fileId, + filePath: dolceFilePath }) - .where(eq(documentAttachments.id, attachment.id)) - uploadResults.push({ - uploadId, - fileId, - filePath: dolceFilePath - }) - - // ResultData 객체 생성 (PWPUploadResultService 호출용) - const fileStats = await this.getFileStats(attachment.filePath) // 파일 통계 정보 조회 - - const resultData: ResultData = { - FileId: fileId, - UploadId: uploadId, - FileSeq: i + 1, // 1부터 시작하는 시퀀스 - FileName: attachment.fileName, - FileRelativePath: dolceFilePath, - FileSize: fileStats.size, - FileCreateDT: fileStats.birthtime.toISOString(), - FileWriteDT: fileStats.mtime.toISOString(), - OwnerUserId: userId - } + // ResultData 객체 생성 (PWPUploadResultService 호출용) + const fileStats = await this.getFileStats(attachment.filePath) // 파일 통계 정보 조회 + + const resultData: ResultData = { + FileId: fileId, + UploadId: uploadId, + FileSeq: i + 1, // 1부터 시작하는 시퀀스 + FileName: attachment.fileName, + FileRelativePath: dolceFilePath, + FileSize: fileStats.size, + FileCreateDT: fileStats.birthtime.toISOString(), + FileWriteDT: fileStats.mtime.toISOString(), + OwnerUserId: userId + } - resultDataArray.push(resultData) + resultDataArray.push(resultData) - console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`) - console.log(`✅ DB updated for attachment ID: ${attachment.id}`) + console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`) + console.log(`✅ DB updated for attachment ID: ${attachment.id}`) - // 🧪 DOLCE 업로드 확인 테스트 - try { - const testResult = await this.testDOLCEFileDownload(fileId, userId, attachment.fileName) - if (testResult.success) { - console.log(`✅ DOLCE 업로드 확인 성공: ${attachment.fileName}`) - } else { - console.warn(`⚠️ DOLCE 업로드 확인 실패: ${attachment.fileName} - ${testResult.error}`) + // 🧪 DOLCE 업로드 확인 테스트 + try { + const testResult = await this.testDOLCEFileDownload(fileId, userId, attachment.fileName) + if (testResult.success) { + console.log(`✅ DOLCE 업로드 확인 성공: ${attachment.fileName}`) + } else { + console.warn(`⚠️ DOLCE 업로드 확인 실패: ${attachment.fileName} - ${testResult.error}`) + } + } catch (testError) { + console.warn(`⚠️ DOLCE 업로드 확인 중 오류: ${attachment.fileName}`, testError) } - } catch (testError) { - console.warn(`⚠️ DOLCE 업로드 확인 중 오류: ${attachment.fileName}`, testError) - } - } catch (error) { - console.error(`❌ File upload failed for ${attachment.fileName}:`, error) - throw error + } catch (error) { + console.error(`❌ File upload failed for ${attachment.fileName}:`, error) + throw error + } } - } - // 모든 파일 업로드가 완료된 후 PWPUploadResultService 호출 - if (resultDataArray.length > 0) { - try { - await this.finalizeUploadResult(resultDataArray) - console.log(`✅ Upload result finalized for UploadId: ${uploadId}`) - } catch (error) { - console.error(`❌ Failed to finalize upload result for UploadId: ${uploadId}`, error) - // 파일 업로드는 성공했지만 결과 저장 실패 - 로그만 남기고 계속 진행 + // 모든 파일 업로드가 완료된 후 PWPUploadResultService 호출 + if (resultDataArray.length > 0) { + try { + await this.finalizeUploadResult(resultDataArray) + console.log(`✅ Upload result finalized for UploadId: ${uploadId}`) + } catch (error) { + console.error(`❌ Failed to finalize upload result for UploadId: ${uploadId}`, error) + // 파일 업로드는 성공했지만 결과 저장 실패 - 로그만 남기고 계속 진행 + } } + + return uploadResults } - return uploadResults -} + private async finalizeUploadResult(resultDataArray: ResultData[]): Promise { + const url = `${this.BASE_URL}/PWPUploadResultService.ashx?` -private async finalizeUploadResult(resultDataArray: ResultData[]): Promise { - const url = `${this.BASE_URL}/PWPUploadResultService.ashx?` - - try { - const jsonData = JSON.stringify(resultDataArray) - const dataBuffer = Buffer.from(jsonData, 'utf-8') + try { + const jsonData = JSON.stringify(resultDataArray) + const dataBuffer = Buffer.from(jsonData, 'utf-8') - console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`) - console.log('ResultData:', JSON.stringify(resultDataArray, null, 2)) + console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`) + console.log('ResultData:', JSON.stringify(resultDataArray, null, 2)) - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: dataBuffer - }) + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: dataBuffer + }) - if (!response.ok) { - const errorText = await response.text() - throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`) - } + if (!response.ok) { + const errorText = await response.text() + throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`) + } - const result = await response.text() - - if (result !== 'Success') { - console.log(result,"돌체 업로드 실패") - throw new Error(`PWPUploadResultService returned unexpected result: ${result}`) - } + const result = await response.text() - console.log('✅ PWPUploadResultService call successful') + if (result !== 'Success') { + console.log(result, "돌체 업로드 실패") + throw new Error(`PWPUploadResultService returned unexpected result: ${result}`) + } - } catch (error) { - console.error('❌ PWPUploadResultService call failed:', error) - throw error - } -} + console.log('✅ PWPUploadResultService call successful') -// 파일 통계 정보 조회 헬퍼 메서드 (파일시스템에서 파일 정보를 가져옴) -private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> { - try { - // Node.js 환경이라면 fs.stat 사용 - const fs = require('fs').promises - const stats = await fs.stat(filePath) - - return { - size: stats.size, - birthtime: stats.birthtime, - mtime: stats.mtime + } catch (error) { + console.error('❌ PWPUploadResultService call failed:', error) + throw error } - } catch (error) { - console.warn(`Could not get file stats for ${filePath}, using defaults`) - // 파일 정보를 가져올 수 없는 경우 기본값 사용 - const now = new Date() - return { - size: 0, - birthtime: now, - mtime: now + } + + // 파일 통계 정보 조회 헬퍼 메서드 (파일시스템에서 파일 정보를 가져옴) + private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> { + try { + // Node.js 환경이라면 fs.stat 사용 + const fs = require('fs').promises + const stats = await fs.stat(filePath) + + return { + size: stats.size, + birthtime: stats.birthtime, + mtime: stats.mtime + } + } catch (error) { + console.warn(`Could not get file stats for ${filePath}, using defaults`) + // 파일 정보를 가져올 수 없는 경우 기본값 사용 + const now = new Date() + return { + size: 0, + birthtime: now, + mtime: now + } } } -} /** * 문서 정보 업로드 (DetailDwgReceiptMgmtEdit) @@ -604,125 +618,125 @@ private async getFileStats(filePath: string): Promise<{ size: number, birthtime: /** * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용) */ -/** - * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용) - */ -private transformToDoLCEDocument( - revision: any, - contractInfo: any, - uploadId?: string, - vendorCode?: string, -): DOLCEDocument { - // Mode 결정: registerId가 있으면 MOD, 없으면 ADD - let mode: "ADD" | "MOD" = "ADD" // 기본값은 ADD - - if (revision.registerId) { - mode = "MOD" - } else { - mode = "ADD" - } + /** + * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용) + */ + private transformToDoLCEDocument( + revision: any, + contractInfo: any, + uploadId?: string, + vendorCode?: string, + ): DOLCEDocument { + // Mode 결정: registerId가 있으면 MOD, 없으면 ADD + let mode: "ADD" | "MOD" = "ADD" // 기본값은 ADD + + if (revision.registerId) { + mode = "MOD" + } else { + mode = "ADD" + } - // RegisterKind 결정: usage와 usageType에 따라 설정 - let registerKind = "APPR" // 기본값 - - if (revision.usage && revision.usage !== 'DEFAULT') { - switch (revision.usage) { - case "APPROVAL": - if (revision.usageType === "Full") { - registerKind = "APPR" - } else if (revision.usageType === "Partial") { - registerKind = "APPR-P" - } else { - registerKind = "APPR" // 기본값 - } - break - - case "WORKING": - if (revision.usageType === "Full") { - registerKind = "WORK" - } else if (revision.usageType === "Partial") { - registerKind = "WORK-P" - } else { - registerKind = "WORK" // 기본값 - } - break + // RegisterKind 결정: usage와 usageType에 따라 설정 + let registerKind = "APPR" // 기본값 + + if (revision.usage && revision.usage !== 'DEFAULT') { + switch (revision.usage) { + case "APPROVAL": + if (revision.usageType === "Full") { + registerKind = "APPR" + } else if (revision.usageType === "Partial") { + registerKind = "APPR-P" + } else { + registerKind = "APPR" // 기본값 + } + break + + case "WORKING": + if (revision.usageType === "Full") { + registerKind = "WORK" + } else if (revision.usageType === "Partial") { + registerKind = "WORK-P" + } else { + registerKind = "WORK" // 기본값 + } + break - case "The 1st": - registerKind = "FMEA-1" - break + case "The 1st": + registerKind = "FMEA-1" + break - case "The 2nd": - registerKind = "FMEA-2" - break + case "The 2nd": + registerKind = "FMEA-2" + break - case "Pre": - registerKind = "RECP" - break + case "Pre": + registerKind = "RECP" + break - case "Working": - registerKind = "RECW" - break + case "Working": + registerKind = "RECW" + break - case "Mark-Up": - registerKind = "CMTM" - break + case "Mark-Up": + registerKind = "CMTM" + break - default: - console.warn(`Unknown usage type: ${revision.usage}, using default APPR`) - registerKind = "APPR" // 기본값 - break + default: + console.warn(`Unknown usage type: ${revision.usage}, using default APPR`) + registerKind = "APPR" // 기본값 + break + } + } else { + console.warn(`No usage specified for revision ${revision.revision}, using default APPR`) } - } else { - console.warn(`No usage specified for revision ${revision.revision}, using default APPR`) - } - // Serial Number 계산 함수 - const getSerialNumber = (revisionValue: string): number => { - if (!revisionValue) { - return 1 - } + // Serial Number 계산 함수 + const getSerialNumber = (revisionValue: string): number => { + if (!revisionValue) { + return 1 + } - // 먼저 숫자인지 확인 - const numericValue = parseInt(revisionValue) - if (!isNaN(numericValue)) { - return numericValue - } + // 먼저 숫자인지 확인 + const numericValue = parseInt(revisionValue) + if (!isNaN(numericValue)) { + return numericValue + } - // 문자인 경우 (a=1, b=2, c=3, ...) - if (typeof revisionValue === 'string' && revisionValue.length === 1) { - const charCode = revisionValue.toLowerCase().charCodeAt(0) - if (charCode >= 97 && charCode <= 122) { // a-z - return charCode - 96 // a=1, b=2, c=3, ... + // 문자인 경우 (a=1, b=2, c=3, ...) + if (typeof revisionValue === 'string' && revisionValue.length === 1) { + const charCode = revisionValue.toLowerCase().charCodeAt(0) + if (charCode >= 97 && charCode <= 122) { // a-z + return charCode - 96 // a=1, b=2, c=3, ... + } } + + // 기본값 + return 1 } - // 기본값 - return 1 - } + console.log(`Transform to DOLCE: Mode=${mode}, RegisterKind=${registerKind}, Usage=${revision.usage}, UsageType=${revision.usageType}`) - console.log(`Transform to DOLCE: Mode=${mode}, RegisterKind=${registerKind}, Usage=${revision.usage}, UsageType=${revision.usageType}`) - - return { - Mode: mode, - Status: revision.revisionStatus || "Standby", - RegisterId: revision.registerId || 0, // registerId가 없으면 0 (ADD 모드) - ProjectNo: contractInfo.projectCode, - Discipline: revision.discipline || "DL", - DrawingKind: revision.drawingKind || "B3", - DrawingNo: revision.documentNo, - DrawingName: revision.documentName, - RegisterGroupId: revision.registerGroupId || 0, - RegisterSerialNo: getSerialNumber(revision.revision || "1"), - RegisterKind: registerKind, // usage/usageType에 따라 동적 설정 - DrawingRevNo: revision.revision || "-", - Category: revision.category || "TS", - Receiver: null, - Manager: revision.managerNo || "202206", // 담당자 번호 사용 - RegisterDesc: revision.comment || "System upload", - UploadId: uploadId, - RegCompanyCode: vendorCode || "A0005531" // 벤더 코드 + return { + Mode: mode, + Status: revision.revisionStatus || "Standby", + RegisterId: revision.registerId || 0, // registerId가 없으면 0 (ADD 모드) + ProjectNo: contractInfo.projectCode, + Discipline: revision.discipline || "DL", + DrawingKind: revision.drawingKind || "B3", + DrawingNo: revision.documentNo, + DrawingName: revision.documentName, + RegisterGroupId: revision.registerGroupId || 0, + RegisterSerialNo: getSerialNumber(revision.revision || "1"), + RegisterKind: registerKind, // usage/usageType에 따라 동적 설정 + DrawingRevNo: revision.revision || "-", + Category: revision.category || "TS", + Receiver: null, + Manager: revision.managerNo || "202206", // 담당자 번호 사용 + RegisterDesc: revision.comment || "System upload", + UploadId: uploadId, + RegCompanyCode: vendorCode || "A0005531" // 벤더 코드 + } } -} /** * 파일 매핑 데이터 변환 */ @@ -769,28 +783,28 @@ private transformToDoLCEDocument( private async getFileBuffer(filePath: string): Promise { try { console.log(`📂 파일 읽기 요청: ${filePath}`); - + if (filePath.startsWith('http')) { // ✅ URL인 경우 직접 다운로드 (기존과 동일) console.log(`🌐 HTTP URL에서 파일 다운로드: ${filePath}`); - + const response = await fetch(filePath); if (!response.ok) { throw new Error(`파일 다운로드 실패: ${response.status}`); } - + const arrayBuffer = await response.arrayBuffer(); console.log(`✅ HTTP 다운로드 완료: ${arrayBuffer.byteLength} bytes`); - + return arrayBuffer; } else { // ✅ 로컬/NAS 파일 경로 처리 (환경별 분기) const fs = await import('fs'); const path = await import('path'); const config = getFileReaderConfig(); - + let actualFilePath: string; - + // 경로 형태별 처리 if (filePath.startsWith('/documents/')) { // ✅ DB에 저장된 경로 형태: "/documents/[uuid].ext" @@ -798,32 +812,48 @@ private transformToDoLCEDocument( // 프로덕션: /evcp_nas/documents/[uuid].ext actualFilePath = path.join(config.baseDir, 'public', filePath.substring(1)); // 앞의 '/' 제거 console.log(`📁 documents 경로 처리: ${filePath} → ${actualFilePath}`); - } - + } + else if (filePath.startsWith('/api/files')) { + + actualFilePath = `${process.env.NEXT_PUBLIC_URL}${filePath}` + + + const response = await fetch(actualFilePath); + if (!response.ok) { + throw new Error(`파일 다운로드 실패: ${response.status}`); + } + + const arrayBuffer = await response.arrayBuffer(); + console.log(`✅ HTTP 다운로드 완료: ${arrayBuffer.byteLength} bytes`); + + return arrayBuffer; + + } + else { // ✅ 상대 경로는 현재 디렉토리 기준 actualFilePath = filePath; console.log(`📂 상대 경로 사용: ${actualFilePath}`); } - + console.log(`🔍 실제 파일 경로: ${actualFilePath}`); console.log(`🏠 환경: ${config.isProduction ? 'PRODUCTION (NAS)' : 'DEVELOPMENT (public)'}`); - + // 파일 존재 여부 확인 if (!fs.existsSync(actualFilePath)) { console.error(`❌ 파일 없음: ${actualFilePath}`); throw new Error(`파일을 찾을 수 없습니다: ${actualFilePath}`); } - + // 파일 읽기 const fileBuffer = fs.readFileSync(actualFilePath); console.log(`✅ 파일 읽기 성공: ${actualFilePath} (${fileBuffer.length} bytes)`); - + // ✅ Buffer를 ArrayBuffer로 정확히 변환 const arrayBuffer = new ArrayBuffer(fileBuffer.length); const uint8Array = new Uint8Array(arrayBuffer); uint8Array.set(fileBuffer); - + return arrayBuffer; } } catch (error) { @@ -881,19 +911,19 @@ private transformToDoLCEDocument( try { // DES 암호화 (C# DESCryptoServiceProvider 호환) const DES_KEY = Buffer.from("4fkkdijg", "ascii") - + // 암호화 문자열 생성: FileId↔UserId↔FileName const encryptString = `${fileId}↔${userId}↔${fileName}` - + // DES 암호화 (createCipheriv 사용) const cipher = crypto.createCipheriv('des-ecb', DES_KEY, '') cipher.setAutoPadding(true) let encrypted = cipher.update(encryptString, 'utf8', 'base64') encrypted += cipher.final('base64') const encryptedKey = encrypted.replace(/\+/g, '|||') - + const downloadUrl = `${process.env.DOLCE_DOWNLOAD_URL}?key=${encryptedKey}` || `http://60.100.99.217:1111/Download.aspx?key=${encryptedKey}` - + console.log(`🧪 DOLCE 파일 다운로드 테스트:`) console.log(` 파일명: ${fileName}`) console.log(` FileId: ${fileId}`) @@ -919,7 +949,7 @@ private transformToDoLCEDocument( const buffer = Buffer.from(await response.arrayBuffer()) console.log(`✅ DOLCE 파일 다운로드 테스트 성공: ${fileName} (${buffer.length} bytes)`) - + return { success: true, downloadUrl diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index 05ace8d5..f2d9c26f 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -2,7 +2,7 @@ "use server" import { revalidatePath, unstable_cache } from "next/cache" -import { and, asc, desc, eq, ilike, or, count, avg, inArray, sql } from "drizzle-orm" +import { and, asc, desc, eq, ilike, or, count, avg, inArray, sql, ne } from "drizzle-orm" import db from "@/db/db" import { StageDocumentsView, documentAttachments, documentStagesOnlyView, documents, enhancedDocumentsView, issueStages, revisions, simplifiedDocumentsView, type EnhancedDocumentsView } from "@/db/schema/vendorDocu" import { filterColumns } from "@/lib/filter-columns" @@ -1175,3 +1175,164 @@ export async function getDocumentDetails(documentId: number) { + export interface UpdateRevisionInput { + revisionId: number + revision: string // ✅ revision 필드 추가 + comment?: string | null + usage: string + usageType?: string | null + } + + export interface UpdateRevisionResult { + success: boolean + message?: string + error?: string + updatedRevision?: any + } + + export async function updateRevisionAction( + input: UpdateRevisionInput + ): Promise { + try { + const { revisionId, revision, comment, usage, usageType } = input + + // 1. 리비전 존재 여부 확인 + const existingRevision = await db + .select() + .from(revisions) + .where(eq(revisions.id, revisionId)) + .limit(1) + + if (!existingRevision || existingRevision.length === 0) { + return { + success: false, + error: "Revision not found" + } + } + + // 2. 동일한 revision 번호가 같은 문서에 이미 존재하는지 확인 (자기 자신 제외) + const duplicateRevision = await db + .select() + .from(revisions) + .innerJoin(issueStages, eq(revisions.issueStageId, issueStages.id)) + .where( + and( + eq(revisions.revision, revision.trim()), + eq(issueStages.documentId, existingRevision[0].issueStageId), // 같은 문서 내에서 + ne(revisions.id, revisionId) // 자기 자신 제외 + ) + ) + .limit(1) + + if (duplicateRevision && duplicateRevision.length > 0) { + return { + success: false, + error: `Revision "${revision.trim()}" already exists in this document` + } + } + + // 3. 첨부파일이 처리된 상태인지 확인 (수정 가능 여부 체크) + const attachments = await db + .select() + .from(documentAttachments) + .where(eq(documentAttachments.revisionId, revisionId)) + + const hasProcessedFiles = attachments.some(att => + att.dolceFilePath && att.dolceFilePath.trim() !== '' + ) + + if (hasProcessedFiles) { + return { + success: false, + error: "Cannot edit revision with processed files" + } + } + + // 4. 리비전 업데이트 + const [updatedRevision] = await db + .update(revisions) + .set({ + revision: revision.trim(), // ✅ revision 필드 업데이트 추가 + comment: comment?.trim() || null, + usage: usage.trim(), + usageType: usageType?.trim() || null, + updatedAt: new Date(), + }) + .where(eq(revisions.id, revisionId)) + .returning() + + revalidatePath("/partners/document-list-ship") // ✅ 경로 오타 수정 + + return { + success: true, + message: `Revision ${revision.trim()} updated successfully`, // ✅ 새 revision 값 사용 + updatedRevision + } + + } catch (error) { + console.error("❌ Revision update server action error:", error) + + return { + success: false, + error: error instanceof Error ? error.message : "Failed to update revision" + } + } + } + // 삭제 서버 액션도 함께 만들어드릴게요 + export interface DeleteRevisionInput { + revisionId: number + } + + export interface DeleteRevisionResult { + success: boolean + message?: string + error?: string + deletedRevisionId?: number + deletedAttachmentsCount?: number + } + + export async function deleteRevisionAction( + input: DeleteRevisionInput + ): Promise { + try { + const { revisionId } = input + + // 1. 리비전과 첨부파일 정보 조회 + const revision = await db + .select() + .from(revisions) + .where(eq(revisions.id, revisionId)) + .limit(1) + + if (!revision || revision.length === 0) { + return { + success: false, + error: "Revision not found" + } + } + + + // 5. 리비전 삭제 + await db + .delete(revisions) + .where(eq(revisions.id, revisionId)) + + // 6. 캐시 재검증 + revalidatePath("/parnters/document-list-ship") + + return { + success: true, + message: `Revision ${revision[0].revision} deleted successfully`, + deletedRevisionId: revisionId, + deletedAttachmentsCount: 0 // revisionAttachments.length + } + + } catch (error) { + console.error("❌ Revision delete server action error:", error) + + return { + success: false, + error: error instanceof Error ? error.message : "Failed to delete revision" + } + } + } \ No newline at end of file diff --git a/lib/vendor-document-list/plant/document-stages-table.tsx b/lib/vendor-document-list/plant/document-stages-table.tsx index f843862d..ccf35f4b 100644 --- a/lib/vendor-document-list/plant/document-stages-table.tsx +++ b/lib/vendor-document-list/plant/document-stages-table.tsx @@ -22,6 +22,8 @@ import { Plus, FileSpreadsheet } from "lucide-react" +import { useTranslation } from "@/i18n/client" +import { useParams } from "next/navigation" import { getDocumentStagesColumns } from "./document-stages-columns" import { ExpandableDataTable } from "@/components/data-table/expandable-data-table" import { toast } from "sonner" @@ -45,6 +47,11 @@ export function DocumentStagesTable({ projectType, }: DocumentStagesTableProps) { const [{ data, pageCount, total }] = React.use(promises) + + // URL에서 언어 파라미터 가져오기 + const params = useParams() + const lng = (params?.lng as string) || 'ko' + const { t } = useTranslation(lng, 'document') // 상태 관리 @@ -160,13 +167,13 @@ export function DocumentStagesTable({ .filter(Boolean) if (stageIds.length > 0) { - toast.success(`${stageIds.length}개 스테이지가 완료 처리되었습니다.`) + toast.success(t('documentList.messages.stageCompletionSuccess', { count: stageIds.length })) } } else if (action === 'bulk_assign') { - toast.info("일괄 담당자 지정 기능은 준비 중입니다.") + toast.info(t('documentList.messages.bulkAssignPending')) } } catch (error) { - toast.error("일괄 작업 중 오류가 발생했습니다.") + toast.error(t('documentList.messages.bulkActionError')) } } @@ -260,47 +267,47 @@ export function DocumentStagesTable({
setQuickFilter('all')}> - 전체 문서 + {t('documentList.dashboard.totalDocuments')}
{stats.total}

- 총 {total}개 문서 + {t('documentList.dashboard.totalDocumentCount', { total })}

setQuickFilter('overdue')}> - 지연 문서 + {t('documentList.dashboard.overdueDocuments')}
{stats.overdue}
-

즉시 확인 필요

+

{t('documentList.dashboard.checkImmediately')}

setQuickFilter('due_soon')}> - 마감 임박 + {t('documentList.dashboard.dueSoonDocuments')}
{stats.dueSoon}
-

3일 이내 마감

+

{t('documentList.dashboard.dueInDays')}

- 평균 진행률 + {t('documentList.dashboard.averageProgress')}
{stats.avgProgress}%
-

전체 프로젝트 진행도

+

{t('documentList.dashboard.overallProgress')}

@@ -312,7 +319,7 @@ export function DocumentStagesTable({ className="cursor-pointer hover:bg-primary hover:text-primary-foreground whitespace-nowrap" onClick={() => setQuickFilter('all')} > - 전체 ({stats.total}) + {t('documentList.quickFilters.all')} ({stats.total}) setQuickFilter('overdue')} > - 지연 ({stats.overdue}) + {t('documentList.quickFilters.overdue')} ({stats.overdue}) setQuickFilter('due_soon')} > - 마감임박 ({stats.dueSoon}) + {t('documentList.quickFilters.dueSoon')} ({stats.dueSoon}) setQuickFilter('in_progress')} > - 진행중 ({stats.inProgress}) + {t('documentList.quickFilters.inProgress')} ({stats.inProgress}) setQuickFilter('high_priority')} > - 높은우선순위 ({stats.highPriority}) + {t('documentList.quickFilters.highPriority')} ({stats.highPriority}) diff --git a/lib/vendor-document-list/ship/send-to-shi-button.tsx b/lib/vendor-document-list/ship/send-to-shi-button.tsx index 447b461b..87cc6ff5 100644 --- a/lib/vendor-document-list/ship/send-to-shi-button.tsx +++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx @@ -28,6 +28,7 @@ import { useClientSyncStatus, useTriggerSync, syncUtils } from "@/hooks/use-sync import type { EnhancedDocument } from "@/types/enhanced-documents" import { useParams } from "next/navigation" import { useTranslation } from "@/i18n/client" +import { useSession } from "next-auth/react" interface SendToSHIButtonProps { documents?: EnhancedDocument[] @@ -43,6 +44,7 @@ export function SendToSHIButton({ const [isDialogOpen, setIsDialogOpen] = React.useState(false) const [syncProgress, setSyncProgress] = React.useState(0) const [currentSyncingContract, setCurrentSyncingContract] = React.useState(null) + const { data: session } = useSession(); const params = useParams() const lng = (params?.lng as string) || "ko" @@ -60,6 +62,8 @@ export function SendToSHIButton({ return uniqueIds.sort() }, [documents]) + const vendorId = session?.user.companyId + // ✅ 클라이언트 전용 Hook 사용 (서버 사이드 렌더링 호환) const { contractStatuses, totalStats, refetchAll } = useClientSyncStatus( documentsContractIds, @@ -68,20 +72,6 @@ export function SendToSHIButton({ const { triggerSync, isLoading: isSyncing, error: syncError } = useTriggerSync() - // 개발 환경에서 디버깅 정보 - React.useEffect(() => { - if (process.env.NODE_ENV === 'development') { - console.log('SendToSHIButton Debug Info:', { - documentsContractIds, - totalStats, - contractStatuses: contractStatuses.map(({ projectId, syncStatus, error }) => ({ - projectId, - pendingChanges: syncStatus?.pendingChanges, - hasError: !!error - })) - }) - } - }, [documentsContractIds, totalStats, contractStatuses]) // 동기화 실행 함수 const handleSync = async () => { diff --git a/lib/vendor-document-list/sync-service.ts b/lib/vendor-document-list/sync-service.ts index 0544ce06..cdc22e11 100644 --- a/lib/vendor-document-list/sync-service.ts +++ b/lib/vendor-document-list/sync-service.ts @@ -8,6 +8,8 @@ import { } from "@/db/schema/vendorDocu" import { documents, revisions, documentAttachments } from "@/db/schema/vendorDocu" import { eq, and, lt, desc, sql, inArray } from "drizzle-orm" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" export interface SyncableEntity { entityType: 'document' | 'revision' | 'attachment' @@ -42,7 +44,7 @@ class SyncService { * 변경사항을 change_logs에 기록 */ async logChange( - projectId: number, + vendorId: number, entityType: 'document' | 'revision' | 'attachment', entityId: number, action: 'CREATE' | 'UPDATE' | 'DELETE', @@ -56,7 +58,7 @@ class SyncService { const changedFields = this.detectChangedFields(oldValues, newValues) await db.insert(changeLogs).values({ - projectId, + vendorId, entityType, entityId, action, @@ -99,7 +101,7 @@ class SyncService { * 동기화할 변경사항 조회 (증분) */ async getPendingChanges( - projectId: number, + vendorId: number, targetSystem: string = 'DOLCE', limit?: number ): Promise { @@ -107,7 +109,7 @@ class SyncService { .select() .from(changeLogs) .where(and( - eq(changeLogs.projectId, projectId), + eq(changeLogs.vendorId, vendorId), eq(changeLogs.isSynced, false), lt(changeLogs.syncAttempts, 3), sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` @@ -136,14 +138,14 @@ class SyncService { * 동기화 배치 생성 */ async createSyncBatch( - projectId: number, + vendorId: number, targetSystem: string, changeLogIds: number[] ): Promise { const [batch] = await db .insert(syncBatches) .values({ - projectId, + vendorId, targetSystem, batchSize: changeLogIds.length, changeLogIds, @@ -168,8 +170,16 @@ class SyncService { throw new Error(`Sync not enabled for ${targetSystem}`) } + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + throw new Error("인증이 필요합니다.") + } + + const vendorId = Number(session.user.companyId) + + // 2. 대기 중인 변경사항 조회 (전체) - const pendingChanges = await this.getPendingChanges(projectId, targetSystem) + const pendingChanges = await this.getPendingChanges(vendorId, targetSystem) if (pendingChanges.length === 0) { return { @@ -182,7 +192,7 @@ class SyncService { // 3. 배치 생성 const batchId = await this.createSyncBatch( - projectId, + vendorId, targetSystem, pendingChanges.map(c => c.id) ) @@ -446,11 +456,20 @@ class SyncService { */ async getSyncStatus(projectId: number, targetSystem: string = 'DOLCE') { try { + + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + throw new Error("인증이 필요합니다.") + } + + const vendorId = Number(session.user.companyId) + + // 대기 중인 변경사항 수 조회 const pendingCount = await db.$count( changeLogs, and( - eq(changeLogs.projectId, projectId), + eq(changeLogs.vendorId, vendorId), eq(changeLogs.isSynced, false), lt(changeLogs.syncAttempts, 3), sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` @@ -461,7 +480,7 @@ class SyncService { const syncedCount = await db.$count( changeLogs, and( - eq(changeLogs.projectId, projectId), + eq(changeLogs.vendorId, vendorId), eq(changeLogs.isSynced, true), sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` ) @@ -471,7 +490,7 @@ class SyncService { const failedCount = await db.$count( changeLogs, and( - eq(changeLogs.projectId, projectId), + eq(changeLogs.vendorId, vendorId), eq(changeLogs.isSynced, false), sql`${changeLogs.syncAttempts} >= 3`, sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` @@ -483,7 +502,7 @@ class SyncService { .select() .from(syncBatches) .where(and( - eq(syncBatches.projectId, projectId), + eq(syncBatches.vendorId, vendorId), eq(syncBatches.targetSystem, targetSystem), eq(syncBatches.status, 'SUCCESS') )) @@ -491,7 +510,7 @@ class SyncService { .limit(1) return { - projectId, + vendorId, targetSystem, totalChanges: pendingCount + syncedCount + failedCount, pendingChanges: pendingCount, @@ -511,11 +530,19 @@ class SyncService { */ async getRecentSyncBatches(projectId: number, targetSystem: string = 'DOLCE', limit: number = 10) { try { + + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + throw new Error("인증이 필요합니다.") + } + + const vendorId = Number(session.user.companyId) + const batches = await db .select() .from(syncBatches) .where(and( - eq(syncBatches.projectId, projectId), + eq(syncBatches.vendorId, vendorId), eq(syncBatches.targetSystem, targetSystem) )) .orderBy(desc(syncBatches.createdAt)) @@ -524,7 +551,7 @@ class SyncService { // Date 객체를 문자열로 변환 return batches.map(batch => ({ id: Number(batch.id), - projectId: batch.projectId, + vendorId: batch.vendorId, targetSystem: batch.targetSystem, batchSize: batch.batchSize, status: batch.status, @@ -561,7 +588,7 @@ export async function logDocumentChange( } export async function logRevisionChange( - projectId: number, + vendorId: number, revisionId: number, action: 'CREATE' | 'UPDATE' | 'DELETE', newValues?: any, @@ -570,11 +597,11 @@ export async function logRevisionChange( userName?: string, targetSystems: string[] = ["DOLCE", "SWP"] ) { - return syncService.logChange(projectId, 'revision', revisionId, action, newValues, oldValues, userId, userName, targetSystems) + return syncService.logChange(vendorId, 'revision', revisionId, action, newValues, oldValues, userId, userName, targetSystems) } export async function logAttachmentChange( - projectId: number, + vendorId: number, attachmentId: number, action: 'CREATE' | 'UPDATE' | 'DELETE', newValues?: any, @@ -583,5 +610,5 @@ export async function logAttachmentChange( userName?: string, targetSystems: string[] = ["DOLCE", "SWP"] ) { - return syncService.logChange(projectId, 'attachment', attachmentId, action, newValues, oldValues, userId, userName, targetSystems) + return syncService.logChange(vendorId, 'attachment', attachmentId, action, newValues, oldValues, userId, userName, targetSystems) } \ No newline at end of file diff --git a/lib/vendor-document-list/table/enhanced-documents-table.tsx b/lib/vendor-document-list/table/enhanced-documents-table.tsx index cb49f796..7e20892e 100644 --- a/lib/vendor-document-list/table/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/table/enhanced-documents-table.tsx @@ -26,6 +26,8 @@ import { Settings, Filter } from "lucide-react" +import { useTranslation } from "@/i18n/client" +import { useParams } from "next/navigation" import { getUpdatedEnhancedColumns } from "./enhanced-doc-table-columns" import { ExpandableDataTable } from "@/components/data-table/expandable-data-table" import { toast } from "sonner" @@ -48,7 +50,7 @@ interface FinalIntegratedDocumentsTableProps { } // ✅ Drawing Kind 옵션 정의 -const DRAWING_KIND_OPTIONS = [ +const DRAWING_KIND_KEYS = [ { value: "all", label: "전체 문서" }, { value: "B3", label: "B3: Vendor" }, { value: "B4", label: "B4: GTT" }, @@ -63,6 +65,20 @@ export function EnhancedDocumentsTable({ initialDrawingKind = "all" }: FinalIntegratedDocumentsTableProps) { const [{ data, pageCount, total }] = React.use(promises) + + // URL에서 언어 파라미터 가져오기 + const params = useParams() + const lng = (params?.lng as string) || 'ko' + const { t } = useTranslation(lng, 'engineering') + console.log(t, 't') + + // ✅ Drawing Kind 옵션 동적 생성 + const DRAWING_KIND_OPTIONS = React.useMemo(() => + DRAWING_KIND_KEYS.map(item => ({ + value: item.value, + label: t(`documentList.drawingKindOptions.${item.value}`) + })) + , [t]) // ✅ Drawing Kind 필터 상태 추가 const [drawingKindFilter, setDrawingKindFilter] = React.useState(initialDrawingKind) @@ -267,13 +283,13 @@ export function EnhancedDocumentsTable({ .filter(Boolean) if (stageIds.length > 0) { - toast.success(`${stageIds.length}개 항목이 승인되었습니다.`) + toast.success(t('documentList.messages.approvalSuccess', { count: stageIds.length })) } } else if (action === 'bulk_upload') { - toast.info("일괄 업로드 기능은 준비 중입니다.") + toast.info(t('documentList.messages.bulkUploadPending')) } } catch (error) { - toast.error("일괄 작업 중 오류가 발생했습니다.") + toast.error(t('documentList.messages.bulkActionError')) } } @@ -318,17 +334,17 @@ export function EnhancedDocumentsTable({ const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "docNumber", - label: "문서번호", + label: t('documentList.filters.documentNumber'), type: "text", }, { id: "title", - label: "문서제목", + label: t('documentList.filters.documentTitle'), type: "text", }, { id: "drawingKind", - label: "문서종류", + label: t('documentList.filters.documentType'), type: "select", options: [ { label: "B3", value: "B3" }, @@ -338,43 +354,43 @@ export function EnhancedDocumentsTable({ }, { id: "currentStageStatus", - label: "스테이지 상태", + label: t('documentList.filters.stageStatus'), type: "select", options: [ - { label: "계획됨", value: "PLANNED" }, - { label: "진행중", value: "IN_PROGRESS" }, - { label: "제출됨", value: "SUBMITTED" }, - { label: "승인됨", value: "APPROVED" }, - { label: "완료됨", value: "COMPLETED" }, + { label: t('documentList.statusOptions.planned'), value: "PLANNED" }, + { label: t('documentList.statusOptions.inProgress'), value: "IN_PROGRESS" }, + { label: t('documentList.statusOptions.submitted'), value: "SUBMITTED" }, + { label: t('documentList.statusOptions.approved'), value: "APPROVED" }, + { label: t('documentList.statusOptions.completed'), value: "COMPLETED" }, ], }, { id: "currentStagePriority", - label: "우선순위", + label: t('documentList.filters.priority'), type: "select", options: [ - { label: "높음", value: "HIGH" }, - { label: "보통", value: "MEDIUM" }, - { label: "낮음", value: "LOW" }, + { label: t('documentList.priorityOptions.high'), value: "HIGH" }, + { label: t('documentList.priorityOptions.medium'), value: "MEDIUM" }, + { label: t('documentList.priorityOptions.low'), value: "LOW" }, ], }, { id: "isOverdue", - label: "지연 여부", + label: t('documentList.filters.overdueStatus'), type: "select", options: [ - { label: "지연됨", value: "true" }, - { label: "정상", value: "false" }, + { label: t('documentList.overdueOptions.overdue'), value: "true" }, + { label: t('documentList.overdueOptions.normal'), value: "false" }, ], }, { id: "currentStageAssigneeName", - label: "담당자", + label: t('documentList.filters.assignee'), type: "text", }, { id: "createdAt", - label: "생성일", + label: t('documentList.filters.createdDate'), type: "date", }, ] @@ -405,48 +421,48 @@ export function EnhancedDocumentsTable({ setQuickFilter('all')}> - {drawingKindFilter === "all" ? "전체 문서" : `${DRAWING_KIND_OPTIONS.find(o => o.value === drawingKindFilter)?.label} 문서`} + {drawingKindFilter === "all" ? t('documentList.dashboard.totalDocuments') : `${DRAWING_KIND_OPTIONS.find(o => o.value === drawingKindFilter)?.label}`}
{stats.total}

- 총 {total}개 중 {stats.total}개 표시 + {t('documentList.dashboard.totalCount', { total, shown: stats.total })}

setQuickFilter('overdue')}> - 지연 문서 + {t('documentList.dashboard.overdueDocuments')}
{stats.overdue}
-

즉시 확인 필요

+

{t('documentList.dashboard.checkImmediately')}

setQuickFilter('due_soon')}> - 마감 임박 + {t('documentList.dashboard.dueSoonDocuments')}
{stats.dueSoon}
-

3일 이내 마감

+

{t('documentList.dashboard.dueInDays')}

- 평균 진행률 + {t('documentList.dashboard.averageProgress')}
{stats.avgProgress}%
-

전체 프로젝트 진행도

+

{t('documentList.dashboard.overallProgress')}

@@ -460,7 +476,7 @@ export function EnhancedDocumentsTable({ className="cursor-pointer hover:bg-primary hover:text-primary-foreground whitespace-nowrap" onClick={() => setQuickFilter('all')} > - 전체 ({stats.total}) + {t('documentList.quickFilters.all')} ({stats.total}) setQuickFilter('overdue')} > - 지연 ({stats.overdue}) + {t('documentList.quickFilters.overdue')} ({stats.overdue}) setQuickFilter('due_soon')} > - 마감임박 ({stats.dueSoon}) + {t('documentList.quickFilters.dueSoon')} ({stats.dueSoon}) setQuickFilter('in_progress')} > - 진행중 ({stats.inProgress}) + {t('documentList.quickFilters.inProgress')} ({stats.inProgress}) setQuickFilter('high_priority')} > - 높은우선순위 ({stats.highPriority}) + {t('documentList.quickFilters.highPriority')} ({stats.highPriority}) @@ -500,7 +516,7 @@ export function EnhancedDocumentsTable({
setItemSearchQuery(e.target.value)} + className="pl-9" + /> +
+ + {/* 검색 결과 드롭다운 */} + {showItemDropdown && ( +
+ {filteredItems.length > 0 ? ( + filteredItems.map((item) => ( + + )) + ) : ( +
+ 검색 결과가 없습니다. +
+ )} +
+ )} + + +
+ 아이템 코드나 이름을 입력하여 검색하고 선택하세요. +
+ + + +
+ + +
+ + + ) +} diff --git a/lib/vendor-regular-registrations/repository.ts b/lib/vendor-regular-registrations/repository.ts index d4c979a5..38bf4aaf 100644 --- a/lib/vendor-regular-registrations/repository.ts +++ b/lib/vendor-regular-registrations/repository.ts @@ -5,8 +5,11 @@ import { vendorAttachments, vendorInvestigationAttachments, basicContract, + basicContractTemplates, vendorPQSubmissions, vendorInvestigations, + vendorBusinessContacts, + vendorAdditionalInfo, } from "@/db/schema"; import { eq, desc, and, sql, inArray } from "drizzle-orm"; import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"; @@ -27,11 +30,16 @@ export async function getVendorRegularRegistrations( assignedDepartment: vendorRegularRegistrations.assignedDepartment, assignedUser: vendorRegularRegistrations.assignedUser, remarks: vendorRegularRegistrations.remarks, + // 새로 추가된 필드들 + safetyQualificationContent: vendorRegularRegistrations.safetyQualificationContent, + gtcSkipped: vendorRegularRegistrations.gtcSkipped, // 벤더 기본 정보 businessNumber: vendors.taxId, companyName: vendors.vendorName, establishmentDate: vendors.createdAt, representative: vendors.representativeName, + // 국가 정보 추가 + country: vendors.country, }) .from(vendorRegularRegistrations) .innerJoin(vendors, eq(vendorRegularRegistrations.vendorId, vendors.id)) @@ -59,23 +67,81 @@ export async function getVendorRegularRegistrations( .innerJoin(vendorInvestigations, eq(vendorInvestigationAttachments.investigationId, vendorInvestigations.id)) .where(inArray(vendorInvestigations.vendorId, vendorIds)) : []; + // 기본 계약 정보 조회 (템플릿별로 가장 최신 것만) + const basicContractsList = vendorIds.length > 0 ? await db + .select({ + vendorId: basicContract.vendorId, + templateId: basicContract.templateId, + status: basicContract.status, + templateName: basicContractTemplates.templateName, + createdAt: basicContract.createdAt, + }) + .from(basicContract) + .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id)) + .where(inArray(basicContract.vendorId, vendorIds)) + .orderBy(desc(basicContract.createdAt)) : []; + + // 추가정보 입력 상태 조회 (업무담당자 정보) + const businessContactsList = vendorIds.length > 0 ? await db + .select({ + vendorId: vendorBusinessContacts.vendorId, + contactType: vendorBusinessContacts.contactType, + }) + .from(vendorBusinessContacts) + .where(inArray(vendorBusinessContacts.vendorId, vendorIds)) : []; + + // 추가정보 테이블 조회 + const additionalInfoList = vendorIds.length > 0 ? await db + .select({ + vendorId: vendorAdditionalInfo.vendorId, + }) + .from(vendorAdditionalInfo) + .where(inArray(vendorAdditionalInfo.vendorId, vendorIds)) : []; + // 각 등록 레코드별로 데이터를 매핑하여 결과 반환 return registrations.map((registration) => { // 벤더별 첨부파일 필터링 const vendorFiles = vendorAttachmentsList.filter(att => att.vendorId === registration.vendorId); const investigationFiles = investigationAttachmentsList.filter(att => att.vendorId === registration.vendorId); + const allVendorContracts = basicContractsList.filter(contract => contract.vendorId === registration.vendorId); + const vendorContacts = businessContactsList.filter(contact => contact.vendorId === registration.vendorId); + const vendorAdditionalInfoData = additionalInfoList.filter(info => info.vendorId === registration.vendorId); + + // 기술자료 동의서, 비밀유지계약서 제외 필터링 + const filteredContracts = allVendorContracts.filter(contract => { + const templateName = contract.templateName?.toLowerCase() || ''; + return !templateName.includes('기술자료') && !templateName.includes('비밀유지'); + }); + + // 템플릿명 기준으로 가장 최신 계약만 유지 (중복 제거) + const vendorContracts = filteredContracts.reduce((acc, contract) => { + const existing = acc.find(c => c.templateName === contract.templateName); + if (!existing || (contract.createdAt && existing.createdAt && contract.createdAt > existing.createdAt)) { + // 기존에 같은 템플릿명이 없거나, 더 최신인 경우 추가/교체 + return acc.filter(c => c.templateName !== contract.templateName).concat(contract); + } + return acc; + }, [] as typeof filteredContracts); // 디버깅을 위한 로그 - console.log(`📋 벤더 ID ${registration.vendorId} (${registration.companyName}) 첨부파일 현황:`, { + console.log(`📋 벤더 ID ${registration.vendorId} (${registration.companyName}) 현황:`, { vendorFiles: vendorFiles.map(f => ({ type: f.attachmentType, fileName: f.fileName })), - investigationFiles: investigationFiles.map(f => ({ type: f.attachmentType, fileName: f.fileName })) + investigationFiles: investigationFiles.map(f => ({ type: f.attachmentType, fileName: f.fileName })), + allContracts: allVendorContracts.length, + uniqueContracts: vendorContracts.map(c => ({ + templateName: c.templateName, + status: c.status, + createdAt: c.createdAt?.toISOString() + })), + contactTypes: vendorContacts.map(c => c.contactType) }); - // 문서 제출 현황 - 실제 첨부파일 존재 여부 확인 (DB 타입명과 정확히 매칭) + // 문서 제출 현황 - 국가별 요구사항 적용 + const isForeign = registration.country !== 'KR'; const documentSubmissionsStatus = { businessRegistration: vendorFiles.some(f => f.attachmentType === "BUSINESS_REGISTRATION"), creditEvaluation: vendorFiles.some(f => f.attachmentType === "CREDIT_REPORT"), - bankCopy: vendorFiles.some(f => f.attachmentType === "BANK_ACCOUNT_COPY"), + bankCopy: isForeign ? vendorFiles.some(f => f.attachmentType === "BANK_ACCOUNT_COPY") : true, // 내자는 통장사본 불필요 auditResult: investigationFiles.length > 0, // 실사 첨부파일이 하나라도 있으면 true }; @@ -88,33 +154,88 @@ export async function getVendorRegularRegistrations( }; // 문서 제출 현황 로그 - console.log(`📊 벤더 ID ${registration.vendorId} 문서 제출 현황:`, documentSubmissionsStatus); + console.log(`📊 벤더 ID ${registration.vendorId} 문서 제출 현황:`, { + documentSubmissionsStatus, + isForeign, + vendorFiles: vendorFiles.map(f => ({ + type: f.attachmentType, + fileName: f.fileName + })), + investigationFilesCount: investigationFiles.length, + country: registration.country + }); - // 계약 동의 현황 (기본값 - 추후 실제 계약 테이블과 연동) + // 계약 동의 현황 - 실제 기본 계약 데이터 기반으로 단순화 const contractAgreementsStatus = { - cp: "not_submitted", - gtc: "not_submitted", - standardSubcontract: "not_submitted", - safetyHealth: "not_submitted", - ethics: "not_submitted", - domesticCredit: "not_submitted", - safetyQualification: "not_submitted", + cp: vendorContracts.some(c => c.status === "COMPLETED") ? "completed" : "not_submitted", + gtc: registration.gtcSkipped ? "completed" : (vendorContracts.some(c => c.templateName?.includes("GTC") && c.status === "COMPLETED") ? "completed" : "not_submitted"), + standardSubcontract: vendorContracts.some(c => c.templateName?.includes("표준하도급") && c.status === "COMPLETED") ? "completed" : "not_submitted", + safetyHealth: vendorContracts.some(c => c.templateName?.includes("안전보건") && c.status === "COMPLETED") ? "completed" : "not_submitted", + ethics: vendorContracts.some(c => c.templateName?.includes("윤리") && c.status === "COMPLETED") ? "completed" : "not_submitted", + domesticCredit: vendorContracts.some(c => c.templateName?.includes("내국신용장") && c.status === "COMPLETED") ? "completed" : "not_submitted", }; + // 추가정보 입력 완료 여부 - 5개 필수 업무담당자 타입 + 추가정보 테이블 모두 입력되었는지 확인 + const requiredContactTypes = ["sales", "design", "delivery", "quality", "tax_invoice"]; + const contactsCompleted = requiredContactTypes.every(type => + vendorContacts.some(contact => contact.contactType === type) + ); + const additionalInfoTableCompleted = vendorAdditionalInfoData.length > 0; + const additionalInfoCompleted = contactsCompleted && additionalInfoTableCompleted; + + // 추가정보 디버깅 로그 + console.log(`🔍 벤더 ID ${registration.vendorId} 추가정보 상세:`, { + requiredContactTypes, + vendorContactTypes: vendorContacts.map(c => c.contactType), + contactsCompleted, + additionalInfoTableCompleted, + additionalInfoData: vendorAdditionalInfoData, + finalAdditionalInfoCompleted: additionalInfoCompleted + }); + + // 모든 조건 충족 여부 확인 + const allDocumentsSubmitted = Object.values(documentSubmissionsStatus).every(status => status === true); + const allContractsCompleted = vendorContracts.length > 0 && vendorContracts.every(c => c.status === "COMPLETED"); + const safetyQualificationCompleted = !!registration.safetyQualificationContent; + + // 모든 조건이 충족되면 status를 "approval_ready"(조건충족)로 자동 변경 + const shouldUpdateStatus = allDocumentsSubmitted && allContractsCompleted && safetyQualificationCompleted && additionalInfoCompleted; + + // 현재 상태가 조건충족이 아닌데 모든 조건이 충족되면 상태 업데이트 + if (shouldUpdateStatus && registration.status !== "approval_ready") { + // 비동기 업데이트 (백그라운드에서 실행) + updateVendorRegularRegistration(registration.id, { + status: "approval_ready" + }).catch(error => { + console.error(`상태 자동 업데이트 실패 (벤더 ID: ${registration.vendorId}):`, error); + }); + } + return { id: registration.id, vendorId: registration.vendorId, - status: registration.status || "audit_pass", + status: shouldUpdateStatus ? "approval_ready" : (registration.status || "audit_pass"), potentialCode: registration.potentialCode, businessNumber: registration.businessNumber || "", companyName: registration.companyName || "", majorItems: registration.majorItems, establishmentDate: registration.establishmentDate?.toISOString() || null, representative: registration.representative, + country: registration.country, documentSubmissions: documentSubmissionsStatus, documentFiles: documentFiles, // 파일 정보 추가 contractAgreements: contractAgreementsStatus, - additionalInfo: true, // TODO: 추가정보 로직 구현 필요 + // 새로 추가된 필드들 + safetyQualificationContent: registration.safetyQualificationContent, + gtcSkipped: registration.gtcSkipped || false, + additionalInfo: additionalInfoCompleted, + // 기본계약 정보 + basicContracts: vendorContracts.map(contract => ({ + templateId: contract.templateId, + templateName: contract.templateName, + status: contract.status, + createdAt: contract.createdAt, + })), registrationRequestDate: registration.registrationRequestDate || null, assignedDepartment: registration.assignedDepartment, assignedUser: registration.assignedUser, @@ -137,6 +258,8 @@ export async function createVendorRegularRegistration(data: { assignedUser?: string; assignedUserCode?: string; remarks?: string; + safetyQualificationContent?: string; + gtcSkipped?: boolean; }) { try { const [registration] = await db @@ -151,6 +274,8 @@ export async function createVendorRegularRegistration(data: { assignedUser: data.assignedUser, assignedUserCode: data.assignedUserCode, remarks: data.remarks, + safetyQualificationContent: data.safetyQualificationContent, + gtcSkipped: data.gtcSkipped || false, }) .returning(); @@ -173,6 +298,8 @@ export async function updateVendorRegularRegistration( assignedUser: string; assignedUserCode: string; remarks: string; + safetyQualificationContent: string; + gtcSkipped: boolean; }> ) { try { diff --git a/lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx b/lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx new file mode 100644 index 00000000..a93fbf22 --- /dev/null +++ b/lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { toast } from "sonner" + +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { updateSafetyQualification } from "./service" + +const formSchema = z.object({ + safetyQualificationContent: z.string().min(1, "안전적격성 평가 내용을 입력해주세요."), +}) + +interface SafetyQualificationUpdateSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + registrationId?: number + vendorName?: string + currentContent?: string | null + onSuccess?: () => void +} + +export function SafetyQualificationUpdateSheet({ + open, + onOpenChange, + registrationId, + vendorName, + currentContent, + onSuccess, +}: SafetyQualificationUpdateSheetProps) { + const [isLoading, setIsLoading] = React.useState(false) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + safetyQualificationContent: currentContent || "", + }, + }) + + // 폼 값 초기화 + React.useEffect(() => { + if (open) { + form.reset({ + safetyQualificationContent: currentContent || "", + }) + } + }, [open, currentContent, form]) + + async function onSubmit(values: z.infer) { + if (!registrationId) { + toast.error("등록 ID가 없습니다.") + return + } + + setIsLoading(true) + try { + const result = await updateSafetyQualification( + registrationId, + values.safetyQualificationContent + ) + + if (result.success) { + toast.success("안전적격성 평가가 등록되었습니다.") + onOpenChange(false) + onSuccess?.() + } else { + toast.error(result.error || "안전적격성 평가 등록에 실패했습니다.") + } + } catch (error) { + console.error("안전적격성 평가 등록 오류:", error) + toast.error("안전적격성 평가 등록 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + 안전적격성 평가 입력 + + {vendorName && `${vendorName}의 `}안전적격성 평가 내용을 입력해주세요. + + + +
+ + ( + + 안전적격성 평가 내용 + +