From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 21 Aug 2025 06:57:36 +0000 Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/basic-contract/service.ts | 88 +- .../add-basic-contract-template-dialog.tsx | 364 ++--- .../template/basic-contract-template-columns.tsx | 314 ++-- .../template/create-revision-dialog.tsx | 21 - .../template/template-editor-wrapper.tsx | 106 +- .../template/update-basicContract-sheet.tsx | 245 +-- lib/basic-contract/validations.ts | 17 +- .../vendor-table/basic-contract-columns.tsx | 96 +- .../vendor-table/basic-contract-sign-dialog.tsx | 313 +++- .../vendor-table/basic-contract-table.tsx | 98 +- .../basicContract-table-toolbar-actions.tsx | 43 +- .../viewer/AutoSignatureFieldDetector.tsx | 493 ++++++ .../viewer/basic-contract-sign-viewer.tsx | 1679 ++++++++++++++++++-- lib/forms/services.ts | 13 +- .../gtc-clauses/table/preview-document-dialog.tsx | 4 +- lib/gtc-contract/status/gtc-contract-table.tsx | 1 - .../status/gtc-documents-table-columns.tsx | 2 +- lib/information/repository.ts | 192 +-- lib/information/service.ts | 618 +++---- .../table/update-information-dialog.tsx | 380 +++-- lib/information/validations.ts | 5 +- lib/menu-list/servcie.ts | 61 +- lib/menu-list/table/menu-list-table.tsx | 54 +- lib/notice/service.ts | 45 +- .../edit-investigation-dialog.tsx | 124 +- lib/pq/pq-review-table-new/site-visit-dialog.tsx | 16 +- lib/pq/service.ts | 57 +- lib/sedp/get-form-tags.ts | 8 +- lib/sedp/sync-form.ts | 4 +- lib/sedp/sync-object-class.ts | 69 +- lib/site-visit/client-site-visit-wrapper.tsx | 19 +- lib/site-visit/shi-attendees-dialog.tsx | 2 +- lib/site-visit/site-visit-detail-dialog.tsx | 2 +- lib/tags/service.ts | 9 +- lib/users/auth/partners-auth.ts | 10 +- lib/users/auth/passwordUtil.ts | 197 ++- lib/vendor-document-list/dolce-upload-service.ts | 3 +- .../enhanced-document-service.ts | 133 ++ lib/vendor-document-list/import-service.ts | 2 + .../ship-all/enhanced-doc-table-columns.tsx | 540 +++++++ .../ship-all/enhanced-documents-table.tsx | 296 ++++ .../ship/import-from-dolce-button.tsx | 38 + lib/vendor-registration-status/repository.ts | 165 -- lib/vendor-registration-status/service.ts | 260 --- lib/vendors/service.ts | 5 +- lib/vendors/table/request-pq-dialog.tsx | 6 +- lib/vendors/validations.ts | 18 +- 47 files changed, 5159 insertions(+), 2076 deletions(-) create mode 100644 lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx create mode 100644 lib/vendor-document-list/ship-all/enhanced-doc-table-columns.tsx create mode 100644 lib/vendor-document-list/ship-all/enhanced-documents-table.tsx delete mode 100644 lib/vendor-registration-status/repository.ts delete mode 100644 lib/vendor-registration-status/service.ts (limited to 'lib') diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 03b27f96..64a50d14 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -4,13 +4,14 @@ import { revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { getErrorMessage } from "@/lib/handle-error"; import { unstable_cache } from "@/lib/unstable-cache"; -import { asc, desc, ilike, inArray, and, gte, lte, not, or, sql, eq, isNull, ne, isNotNull, count } from "drizzle-orm"; +import { asc, desc, ilike, inArray, and, gte, lte, not, or, sql, eq, isNull, ne, isNotNull, count,like } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { basicContract, BasicContractTemplate, basicContractTemplates, basicContractView, + vendorAttachments, vendors, type BasicContractTemplate as DBBasicContractTemplate, } from "@/db/schema"; @@ -195,15 +196,6 @@ export async function createBasicContractTemplate(input: CreateBasicContractTemp const [row] = await insertBasicContractTemplate(tx, { templateName: input.templateName, revision: input.revision || 1, - legalReviewRequired: input.legalReviewRequired, - shipBuildingApplicable: input.shipBuildingApplicable, - windApplicable: input.windApplicable, - pcApplicable: input.pcApplicable, - nbApplicable: input.nbApplicable, - rcApplicable: input.rcApplicable, - gyApplicable: input.gyApplicable, - sysApplicable: input.sysApplicable, - infraApplicable: input.infraApplicable, status: input.status || "ACTIVE", // 📝 null 처리 추가 @@ -675,8 +667,8 @@ export async function getBasicContractsByVendorId( input: GetBasciContractsSchema, vendorId: number ) { - return unstable_cache( - async () => { + // return unstable_cache( + // async () => { try { const offset = (input.page - 1) * input.perPage; @@ -743,13 +735,13 @@ export async function getBasicContractsByVendorId( // 에러 발생 시 디폴트 return { data: [], pageCount: 0 }; } - }, - [JSON.stringify(input), String(vendorId)], // 캐싱 키에 vendorId 추가 - { - revalidate: 3600, - tags: ["basicContractView-vendor"], // revalidateTag("basicContractView") 호출 시 무효화 - } - )(); + // }, + // [JSON.stringify(input), String(vendorId)], // 캐싱 키에 vendorId 추가 + // { + // revalidate: 3600, + // tags: ["basicContractView-vendor"], // revalidateTag("basicContractView") 호출 시 무효화 + // } + // )(); } export async function getAllTemplates(): Promise { @@ -1120,15 +1112,25 @@ export async function getALLBasicContractTemplates() { // 2) 등록된 templateName만 중복 없이 가져오기 export async function getExistingTemplateNames(): Promise { - const rows = await db - .select({ - templateName: basicContractTemplates.templateName, - }) - .from(basicContractTemplates) - .where(eq(basicContractTemplates.status,"ACTIVE")) - .groupBy(basicContractTemplates.templateName); - - return rows.map((r) => r.templateName); + try { + const templates = await db + .select({ + templateName: basicContractTemplates.templateName + }) + .from(basicContractTemplates) + .where( + and( + eq(basicContractTemplates.status, 'ACTIVE'), + // GTC가 아닌 것들만 중복 체크 (GTC는 프로젝트별로 여러 개 허용) + not(like(basicContractTemplates.templateName, '% GTC')) + ) + ); + + return templates.map(t => t.templateName); + } catch (error) { + console.error('Failed to fetch existing template names:', error); + throw new Error('기존 템플릿 이름을 가져오는데 실패했습니다.'); + } } export async function getExistingTemplateNamesById(id:number): Promise { @@ -1141,4 +1143,32 @@ export async function getExistingTemplateNamesById(id:number): Promise { .limit(1) return rows[0].templateName; -} \ No newline at end of file +} + +export async function getVendorAttachments(vendorId: number) { + try { + const attachments = await db + .select() + .from(vendorAttachments) + .where( + and( + eq(vendorAttachments.vendorId, vendorId), + eq(vendorAttachments.attachmentType, "NDA_ATTACHMENT") + ) + ); + + console.log(attachments,"attachments") + + return { + success: true, + data: attachments + }; + } catch (error) { + console.error("Error fetching vendor attachments:", error); + return { + success: false, + data: [], + error: "Failed to fetch vendor attachments" + }; + } +} diff --git a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx index 43c19e67..141cb1e3 100644 --- a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -16,9 +16,7 @@ import { FormMessage, FormDescription, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; import { Switch } from "@/components/ui/switch"; import { Select, @@ -37,21 +35,18 @@ import { } from "@/components/ui/dropzone"; import { Progress } from "@/components/ui/progress"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; import { useRouter } from "next/navigation"; -import { BUSINESS_UNITS } from "@/config/basicContractColumnsConfig"; import { getExistingTemplateNames } from "../service"; +import { getAvailableProjectsForGtc } from "@/lib/gtc-contract/service"; -// ✅ 서버 액션 import - -// 전체 템플릿 후보 -const TEMPLATE_NAME_OPTIONS = [ +// 고정 템플릿 옵션들 (GTC 제외) +const FIXED_TEMPLATE_OPTIONS = [ "준법서약 (한글)", "준법서약 (영문)", "기술자료 요구서", "비밀유지 계약서", "표준하도급기본 계약서", - "GTC", + "General GTC", // 기본 GTC (하나만) "안전보건관리 약정서", "동반성장", "윤리규범 준수 서약서", @@ -60,28 +55,33 @@ const TEMPLATE_NAME_OPTIONS = [ "직납자재 하도급대급등 연동제 의향서" ] as const; +// 프로젝트 타입 정의 +type ProjectForFilter = { + id: number; + code: string; + name: string; +}; + const templateFormSchema = z.object({ - templateName: z.enum(TEMPLATE_NAME_OPTIONS, { - required_error: "템플릿 이름을 선택해주세요.", + templateType: z.enum(['FIXED', 'PROJECT_GTC'], { + required_error: "템플릿 타입을 선택해주세요.", }), + templateName: z.string().min(1, "템플릿 이름을 선택하거나 입력해주세요."), + selectedProjectId: z.number().optional(), legalReviewRequired: z.boolean().default(false), - // 적용 범위 - shipBuildingApplicable: z.boolean().default(false), - windApplicable: z.boolean().default(false), - pcApplicable: z.boolean().default(false), - nbApplicable: z.boolean().default(false), - rcApplicable: z.boolean().default(false), - gyApplicable: z.boolean().default(false), - sysApplicable: z.boolean().default(false), - infraApplicable: z.boolean().default(false), - file: z.instanceof(File).optional(), + file: z.instanceof(File, { + message: "파일을 업로드해주세요.", + }), }) .refine((data) => { - if (data.templateName !== "General GTC" && !data.file) return false; + // PROJECT_GTC 타입인 경우 프로젝트 선택 필수 + if (data.templateType === 'PROJECT_GTC' && !data.selectedProjectId) { + return false; + } return true; }, { - message: "파일을 업로드해주세요.", - path: ["file"], + message: "프로젝트를 선택해주세요.", + path: ["selectedProjectId"], }) .refine((data) => { if (data.file && data.file.size > 100 * 1024 * 1024) return false; @@ -100,16 +100,6 @@ const templateFormSchema = z.object({ }, { message: "워드 파일(.doc, .docx)만 업로드 가능합니다.", path: ["file"], -}) -.refine((data) => { - const scopeFields = [ - 'shipBuildingApplicable', 'windApplicable', 'pcApplicable', 'nbApplicable', - 'rcApplicable', 'gyApplicable', 'sysApplicable', 'infraApplicable' - ]; - return scopeFields.some(field => data[field as keyof typeof data] === true); -}, { - message: "적어도 하나의 적용 범위를 선택해야 합니다.", - path: ["shipBuildingApplicable"], }); type TemplateFormValues = z.infer; @@ -120,21 +110,16 @@ export function AddTemplateDialog() { const [selectedFile, setSelectedFile] = React.useState(null); const [uploadProgress, setUploadProgress] = React.useState(0); const [showProgress, setShowProgress] = React.useState(false); - const [availableTemplateNames, setAvailableTemplateNames] = React.useState(TEMPLATE_NAME_OPTIONS); + const [availableFixedTemplates, setAvailableFixedTemplates] = React.useState([]); + const [availableProjects, setAvailableProjects] = React.useState([]); const router = useRouter(); // 기본값 const defaultValues: Partial = { - templateName: undefined, + templateType: 'FIXED', + templateName: '', + selectedProjectId: undefined, legalReviewRequired: false, - shipBuildingApplicable: false, - windApplicable: false, - pcApplicable: false, - nbApplicable: false, - rcApplicable: false, - gyApplicable: false, - sysApplicable: false, - infraApplicable: false, }; const form = useForm({ @@ -143,24 +128,38 @@ export function AddTemplateDialog() { mode: "onChange", }); - // 🔸 마운트 시 이미 등록된 templateName 목록 가져와서 필터링 + // 🔸 마운트 시 사용 가능한 고정 템플릿들과 프로젝트들 가져오기 React.useEffect(() => { let cancelled = false; - (async () => { + + const loadData = async () => { try { - const usedNames = await getExistingTemplateNames(); + // 고정 템플릿 중 이미 사용된 것들 제외 + const usedTemplateNames = await getExistingTemplateNames(); if (cancelled) return; - // 이미 있는 이름 제외 - const filtered = TEMPLATE_NAME_OPTIONS.filter(name => !usedNames.includes(name)); - setAvailableTemplateNames(filtered); + const filteredFixedTemplates = FIXED_TEMPLATE_OPTIONS.filter( + name => !usedTemplateNames.includes(name) + ); + setAvailableFixedTemplates(filteredFixedTemplates); + + // GTC 생성 가능한 프로젝트들 가져오기 + const projects = await getAvailableProjectsForGtc(); + if (cancelled) return; + + setAvailableProjects(projects); } catch (err) { - console.error("Failed to fetch existing template names", err); - // 실패 시 전체 옵션 보여주거나, 오류 알려주기 + console.error("Failed to load template data", err); + toast.error("템플릿 정보를 불러오는데 실패했습니다."); } - })(); + }; + + if (open) { + loadData(); + } + return () => { cancelled = true; }; - }, []); + }, [open]); const handleFileChange = (files: File[]) => { if (files.length > 0) { @@ -170,10 +169,13 @@ export function AddTemplateDialog() { } }; - const handleSelectAllScopes = (checked: boolean) => { - BUSINESS_UNITS.forEach(unit => { - form.setValue(unit.key as keyof TemplateFormValues, checked); - }); + // 프로젝트 선택 시 템플릿 이름 자동 설정 + const handleProjectChange = (projectId: string) => { + const project = availableProjects.find(p => p.id === parseInt(projectId)); + if (project) { + form.setValue("selectedProjectId", project.id); + form.setValue("templateName", `${project.code} GTC`); + } }; // 청크 업로드 설정 @@ -218,22 +220,14 @@ export function AddTemplateDialog() { async function onSubmit(formData: TemplateFormValues) { setIsLoading(true); try { - let uploadResult = null; - - // 📝 파일 업로드가 필요한 경우에만 업로드 진행 - if (formData.file) { - const fileId = uuidv4(); - uploadResult = await uploadFileInChunks(formData.file, fileId); - - if (!uploadResult?.success) { - throw new Error("파일 업로드에 실패했습니다."); - } + // 파일 업로드 진행 + const fileId = uuidv4(); + const uploadResult = await uploadFileInChunks(formData.file, fileId); + + if (!uploadResult?.success) { + throw new Error("파일 업로드에 실패했습니다."); } - - // 📝 General GTC이고 파일이 없는 경우와 다른 경우 구분 처리 - const isGeneralGTC = formData.templateName === "General GTC"; - const hasFile = uploadResult && uploadResult.success; - + const saveResponse = await fetch('/api/upload/basicContract/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -241,37 +235,19 @@ export function AddTemplateDialog() { templateName: formData.templateName, revision: 1, legalReviewRequired: formData.legalReviewRequired, - shipBuildingApplicable: formData.shipBuildingApplicable, - windApplicable: formData.windApplicable, - pcApplicable: formData.pcApplicable, - nbApplicable: formData.nbApplicable, - rcApplicable: formData.rcApplicable, - gyApplicable: formData.gyApplicable, - sysApplicable: formData.sysApplicable, - infraApplicable: formData.infraApplicable, status: "ACTIVE", - - // 📝 파일이 있는 경우에만 fileName과 filePath 전송 - ...(hasFile && { - fileName: uploadResult.fileName, - filePath: uploadResult.filePath, - }), - - // 📝 파일이 없는 경우 null 전송 (스키마가 nullable이어야 함) - ...(!hasFile && { - fileName: null, - filePath: null, - }) + fileName: uploadResult.fileName, + filePath: uploadResult.filePath, }), next: { tags: ["basic-contract-templates"] }, }); - + const saveResult = await saveResponse.json(); if (!saveResult.success) { console.log(saveResult.error); throw new Error(saveResult.error || "템플릿 정보 저장에 실패했습니다."); } - + toast.success('템플릿이 성공적으로 추가되었습니다.'); form.reset(); setSelectedFile(null); @@ -302,16 +278,15 @@ export function AddTemplateDialog() { setOpen(nextOpen); } - const selectedScopesCount = BUSINESS_UNITS.filter(unit => - form.watch(unit.key as keyof TemplateFormValues) - ).length; - - const templateNameIsRequired = form.watch("templateName") !== "General GTC"; + const templateType = form.watch("templateType"); + const selectedProjectId = form.watch("selectedProjectId"); + const templateName = form.watch("templateName"); const isSubmitDisabled = isLoading || - !form.watch("templateName") || - (templateNameIsRequired && !form.watch("file")) || - !BUSINESS_UNITS.some(unit => form.watch(unit.key as keyof TemplateFormValues)); + !templateType || + !templateName || + !form.watch("file") || + (templateType === 'PROJECT_GTC' && !selectedProjectId); return ( @@ -332,13 +307,61 @@ export function AddTemplateDialog() {
+ {/* 템플릿 타입 선택 */} + + + 템플릿 종류 + + 추가할 템플릿의 종류를 선택하세요 + + + + ( + + + 템플릿 종류 * + + + + {templateType === 'FIXED' && "미리 정의된 표준 템플릿 중에서 선택합니다."} + {templateType === 'PROJECT_GTC' && "특정 프로젝트용 GTC 템플릿을 생성합니다."} + + + + )} + /> + + + {/* 기본 정보 */} 기본 정보 -
+ {/* 표준 템플릿 선택 */} + {templateType === 'FIXED' && ( + + + + + + + {availableProjects.map((project) => ( + + {project.code} - {project.name} + + ))} + + + + 아직 GTC가 생성되지 않은 프로젝트만 표시됩니다. + + + + )} + /> + + {/* 생성될 템플릿 이름 미리보기 */} + {templateName && ( +
+
생성될 템플릿 이름
+
{templateName}
+
+ )} + + )} - {/* 적용 범위 */} - - - - 적용 범위 * - - - 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨) - - - -
- - -
- - - -
- {BUSINESS_UNITS.map((unit) => ( - ( - - - - -
- - {unit.label} - -
-
- )} - /> - ))} -
- - {form.formState.errors.shipBuildingApplicable && ( -

- {form.formState.errors.shipBuildingApplicable.message} -

- )} -
-
- {/* 파일 업로드 */} 파일 업로드 - {form.watch("templateName") === "General GTC" - ? "General GTC는 파일 업로드가 선택사항입니다" - : "템플릿 파일을 업로드하세요"} + 템플릿 파일을 업로드하세요 @@ -471,13 +485,7 @@ export function AddTemplateDialog() { render={() => ( - 템플릿 파일 - {form.watch("templateName") !== "General GTC" && ( - * - )} - {form.watch("templateName") === "General GTC" && ( - (선택사항) - )} + 템플릿 파일 * {selectedFile ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` - : form.watch("templateName") === "General GTC" - ? "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (선택사항, 최대 100MB)" - : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} + : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} @@ -543,4 +549,4 @@ export function AddTemplateDialog() {
); -} +} \ No newline at end of file diff --git a/lib/basic-contract/template/basic-contract-template-columns.tsx b/lib/basic-contract/template/basic-contract-template-columns.tsx index 446112db..a0bef7bf 100644 --- a/lib/basic-contract/template/basic-contract-template-columns.tsx +++ b/lib/basic-contract/template/basic-contract-template-columns.tsx @@ -119,13 +119,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef const template = row.original; const handleViewDetails = () => { - // templateName이 "General GTC"인 경우 특별한 라우팅 - if (template.templateName === "GTC") { - router.push(`/evcp/basic-contract-template/gtc`); - } else { - // 일반적인 경우는 기존과 동일 router.push(`/evcp/basic-contract-template/${template.id}`); - } }; return ( @@ -221,12 +215,10 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef const template = row.original; const handleClick = () => { - if (template.templateName === "GTC") { - router.push(`/evcp/basic-contract-template/gtc`); - } else { + // 일반적인 경우는 기존과 동일 router.push(`/evcp/basic-contract-template/${template.id}`); - } + }; return ( @@ -277,152 +269,152 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef ]; // 적용 범위 그룹 - const scopeColumns: ColumnDef[] = [ - { - accessorKey: "shipBuildingApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("shipBuildingApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 80, - enableResizing: true, - }, - { - accessorKey: "windApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("windApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 60, - enableResizing: true, - }, - { - accessorKey: "pcApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("pcApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 50, - enableResizing: true, - }, - { - accessorKey: "nbApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("nbApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 50, - enableResizing: true, - }, - { - accessorKey: "rcApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("rcApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 50, - enableResizing: true, - }, - { - accessorKey: "gyApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("gyApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 50, - enableResizing: true, - }, - { - accessorKey: "sysApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("sysApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 60, - enableResizing: true, - }, - { - accessorKey: "infraApplicable", - header: ({ column }) => , - cell: ({ row }) => { - const applicable = row.getValue("infraApplicable") as boolean; - return ( -
- {applicable ? ( - - ) : ( - - )} -
- ); - }, - size: 60, - enableResizing: true, - }, - ]; + // const scopeColumns: ColumnDef[] = [ + // { + // accessorKey: "shipBuildingApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("shipBuildingApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 80, + // enableResizing: true, + // }, + // { + // accessorKey: "windApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("windApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 60, + // enableResizing: true, + // }, + // { + // accessorKey: "pcApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("pcApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 50, + // enableResizing: true, + // }, + // { + // accessorKey: "nbApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("nbApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 50, + // enableResizing: true, + // }, + // { + // accessorKey: "rcApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("rcApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 50, + // enableResizing: true, + // }, + // { + // accessorKey: "gyApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("gyApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 50, + // enableResizing: true, + // }, + // { + // accessorKey: "sysApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("sysApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 60, + // enableResizing: true, + // }, + // { + // accessorKey: "infraApplicable", + // header: ({ column }) => , + // cell: ({ row }) => { + // const applicable = row.getValue("infraApplicable") as boolean; + // return ( + //
+ // {applicable ? ( + // + // ) : ( + // + // )} + //
+ // ); + // }, + // size: 60, + // enableResizing: true, + // }, + // ]; // 파일 정보 그룹 const fileInfoColumns: ColumnDef[] = [ @@ -495,11 +487,11 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef header: "기본 정보", columns: basicInfoColumns, }, - { - id: "적용 범위", - header: "적용 범위", - columns: scopeColumns, - }, + // { + // id: "적용 범위", + // header: "적용 범위", + // columns: scopeColumns, + // }, { id: "파일 정보", header: "파일 정보", diff --git a/lib/basic-contract/template/create-revision-dialog.tsx b/lib/basic-contract/template/create-revision-dialog.tsx index 262df6ba..6ae03cc2 100644 --- a/lib/basic-contract/template/create-revision-dialog.tsx +++ b/lib/basic-contract/template/create-revision-dialog.tsx @@ -65,15 +65,6 @@ const createRevisionSchema = z.object({ revision: z.coerce.number().int().min(1), legalReviewRequired: z.boolean().default(false), - // 적용 범위 - shipBuildingApplicable: z.boolean().default(false), - windApplicable: z.boolean().default(false), - pcApplicable: z.boolean().default(false), - nbApplicable: z.boolean().default(false), - rcApplicable: z.boolean().default(false), - gyApplicable: z.boolean().default(false), - sysApplicable: z.boolean().default(false), - infraApplicable: z.boolean().default(false), file: z .instanceof(File, { message: "파일을 업로드해주세요." }) @@ -86,18 +77,6 @@ const createRevisionSchema = z.object({ file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', { message: "워드 파일(.doc, .docx)만 업로드 가능합니다." } ), -}).refine((data) => { - // 적어도 하나의 적용 범위는 선택되어야 함 - const scopeFields = [ - 'shipBuildingApplicable', 'windApplicable', 'pcApplicable', 'nbApplicable', - 'rcApplicable', 'gyApplicable', 'sysApplicable', 'infraApplicable' - ]; - - const hasAnyScope = scopeFields.some(field => data[field as keyof typeof data] === true); - return hasAnyScope; -}, { - message: "적어도 하나의 적용 범위를 선택해야 합니다.", - path: ["shipBuildingApplicable"], }); type CreateRevisionFormValues = z.infer; diff --git a/lib/basic-contract/template/template-editor-wrapper.tsx b/lib/basic-contract/template/template-editor-wrapper.tsx index 96e2330f..af5d42a8 100644 --- a/lib/basic-contract/template/template-editor-wrapper.tsx +++ b/lib/basic-contract/template/template-editor-wrapper.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { Save, RefreshCw, Type, FileText, AlertCircle } from "lucide-react"; import type { WebViewerInstance } from "@pdftron/webviewer"; import { Badge } from "@/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { BasicContractTemplateViewer } from "./basic-contract-template-viewer"; import { getExistingTemplateNamesById, saveTemplateFile } from "../service"; @@ -16,20 +17,57 @@ interface TemplateEditorWrapperProps { refreshAction?: () => Promise; } -// 템플릿 이름별 변수 매핑 (영문 변수명 사용) +const getVariablesForTemplate = (templateName: string): string[] => { + // 정확한 매치 먼저 확인 + if (TEMPLATE_VARIABLES_MAP[templateName as keyof typeof TEMPLATE_VARIABLES_MAP]) { + return [...TEMPLATE_VARIABLES_MAP[templateName as keyof typeof TEMPLATE_VARIABLES_MAP]]; + } + + // GTC가 포함된 경우 확인 + if (templateName.includes("GTC")) { + return [...TEMPLATE_VARIABLES_MAP["GTC"]]; + } + + // 다른 키워드들도 포함 관계로 확인 + for (const [key, variables] of Object.entries(TEMPLATE_VARIABLES_MAP)) { + if (templateName.includes(key)) { + return [...variables]; + } + } + + // 기본값 반환 + return ["company_name", "company_address", "representative_name", "signature_date"]; +}; + +// 템플릿 이름별 변수 매핑 const TEMPLATE_VARIABLES_MAP = { - "준법서약 (한글)": ["vendor_name", "address", "representative_name", "today_date"], - "준법서약 (영문)": ["vendor_name", "address", "representative_name", "today_date"], - "기술자료 요구서": ["vendor_name", "address", "representative_name", "today_date"], - "비밀유지 계약서": ["vendor_name", "address", "representative_name", "today_date"], - "표준하도급기본 계약서": ["vendor_name", "address", "representative_name", "today_date"], - "GTC": ["vendor_name", "address", "representative_name", "today_date"], - "안전보건관리 약정서": ["vendor_name", "address", "representative_name", "today_date"], - "동반성장": ["vendor_name", "address", "representative_name", "today_date"], - "윤리규범 준수 서약서": ["vendor_name", "address", "representative_name", "today_date"], - "기술자료 동의서": ["vendor_name", "address", "representative_name", "today_date"], - "내국신용장 미개설 합의서": ["vendor_name", "address", "representative_name", "today_date"], - "직납자재 하도급대급등 연동제 의향서": ["vendor_name", "address", "representative_name", "today_date"] + "준법서약 (한글)": ["company_name", "company_address", "representative_name", "signature_date"], + "준법서약 (영문)": ["company_name", "company_address", "representative_name", "signature_date"], + "기술자료 요구서": ["company_name", "company_address", "representative_name", "signature_date", 'tax_id', 'phone_number'], + "비밀유지 계약서": ["company_name", "company_address", "representative_name", "signature_date"], + "표준하도급기본 계약서": ["company_name", "company_address", "representative_name", "signature_date"], + "GTC": ["company_name", "company_address", "representative_name", "signature_date"], + "안전보건관리 약정서": ["company_name", "company_address", "representative_name", "signature_date"], + "동반성장": ["company_name", "company_address", "representative_name", "signature_date"], + "윤리규범 준수 서약서": ["company_name", "company_address", "representative_name", "signature_date"], + "기술자료 동의서": ["company_name", "company_address", "representative_name", "signature_date", 'tax_id', 'phone_number'], + "내국신용장 미개설 합의서": ["company_name", "company_address", "representative_name", "signature_date"], + "직납자재 하도급대급등 연동제 의향서": ["company_name", "company_address", "representative_name", "signature_date"] +} as const; + +// 변수별 한글 설명 매핑 +const VARIABLE_DESCRIPTION_MAP = { + "company_name": "협력회사명", + "vendor_name": "협력회사명", + "company_address": "회사주소", + "address": "회사주소", + "representative_name": "대표자명", + "signature_date": "서명날짜", + "today_date": "오늘날짜", + "tax_id": "사업자등록번호", + "phone_number": "전화번호", + "phone": "전화번호", + "email": "이메일" } as const; // 변수 패턴 감지를 위한 정규식 @@ -49,8 +87,6 @@ export function TemplateEditorWrapper({ const [templateName, setTemplateName] = React.useState(""); const [predefinedVariables, setPredefinedVariables] = React.useState([]); - console.log(templateId, "templateId"); - // 템플릿 이름 로드 및 변수 설정 React.useEffect(() => { const loadTemplateInfo = async () => { @@ -59,15 +95,15 @@ export function TemplateEditorWrapper({ setTemplateName(name); // 템플릿 이름에 따른 변수 설정 - const variables = TEMPLATE_VARIABLES_MAP[name as keyof typeof TEMPLATE_VARIABLES_MAP] || []; - setPredefinedVariables(variables); + const variables = getVariablesForTemplate(name); + setPredefinedVariables([...variables]); console.log("🏷️ 템플릿 이름:", name); console.log("📝 할당된 변수들:", variables); } catch (error) { console.error("템플릿 정보 로드 오류:", error); // 기본 변수 설정 - setPredefinedVariables(["회사명", "주소", "대표자명", "오늘날짜"]); + setPredefinedVariables(["company_name", "company_address", "representative_name", "signature_date"]); } }; @@ -358,19 +394,27 @@ export function TemplateEditorWrapper({

{templateName ? `${templateName}에 권장되는 변수` : "자주 사용하는 변수"} (클릭하여 복사):

-
- {predefinedVariables.map((variable, index) => ( - - ))} -
+ +
+ {predefinedVariables.map((variable, index) => ( + + + + + +

{VARIABLE_DESCRIPTION_MAP[variable as keyof typeof VARIABLE_DESCRIPTION_MAP] || variable}

+
+
+ ))} +
+
)} diff --git a/lib/basic-contract/template/update-basicContract-sheet.tsx b/lib/basic-contract/template/update-basicContract-sheet.tsx index 07bac31b..0236fda5 100644 --- a/lib/basic-contract/template/update-basicContract-sheet.tsx +++ b/lib/basic-contract/template/update-basicContract-sheet.tsx @@ -8,7 +8,6 @@ import { toast } from "sonner" import * as z from "zod" import { Button } from "@/components/ui/button" -import { Checkbox } from "@/components/ui/checkbox" import { Switch } from "@/components/ui/switch" import { Form, @@ -19,14 +18,6 @@ import { FormMessage, FormDescription, } from "@/components/ui/form" -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { Sheet, SheetClose, @@ -45,45 +36,14 @@ import { DropzoneInput } from "@/components/ui/dropzone" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Separator } from "@/components/ui/separator" import { Badge } from "@/components/ui/badge" import { updateTemplate } from "../service" import { BasicContractTemplate } from "@/db/schema" -import { BUSINESS_UNITS, scopeHelpers } from "@/config/basicContractColumnsConfig" - -// 템플릿 이름 옵션 정의 -const TEMPLATE_NAME_OPTIONS = [ - "준법서약 (한글)", - "준법서약 (영문)", - "기술자료 요구서", - "비밀유지 계약서", - "표준하도급기본 계약서", - "GTC", - "안전보건관리 약정서", - "동반성장", - "윤리규범 준수 서약서", - "기술자료 동의서", - "내국신용장 미개설 합의서", - "직납자재 하도급대급등 연동제 의향서" -] as const; +import { scopeHelpers } from "@/config/basicContractColumnsConfig" -// 업데이트 템플릿 스키마 정의 (리비전 필드 제거, 워드파일만 허용) +// 업데이트 템플릿 스키마 정의 (파일 업데이트 중심) export const updateTemplateSchema = z.object({ - templateName: z.enum(TEMPLATE_NAME_OPTIONS, { - required_error: "템플릿 이름을 선택해주세요.", - }), legalReviewRequired: z.boolean(), - - // 적용 범위 - shipBuildingApplicable: z.boolean(), - windApplicable: z.boolean(), - pcApplicable: z.boolean(), - nbApplicable: z.boolean(), - rcApplicable: z.boolean(), - gyApplicable: z.boolean(), - sysApplicable: z.boolean(), - infraApplicable: z.boolean(), - file: z .instanceof(File, { message: "파일을 업로드해주세요." }) .refine((file) => file.size <= 100 * 1024 * 1024, { @@ -96,15 +56,6 @@ export const updateTemplateSchema = z.object({ { message: "워드 파일(.doc, .docx)만 업로드 가능합니다." } ) .optional(), -}).refine((data) => { - // 적어도 하나의 적용 범위는 선택되어야 함 - const hasAnyScope = BUSINESS_UNITS.some(unit => - data[unit.key as keyof typeof data] as boolean - ); - return hasAnyScope; -}, { - message: "적어도 하나의 적용 범위를 선택해야 합니다.", - path: ["shipBuildingApplicable"], }); export type UpdateTemplateSchema = z.infer @@ -122,16 +73,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem const form = useForm({ resolver: zodResolver(updateTemplateSchema), defaultValues: { - templateName: template?.templateName as typeof TEMPLATE_NAME_OPTIONS[number] ?? "준법서약 (한글)", legalReviewRequired: template?.legalReviewRequired ?? false, - shipBuildingApplicable: template?.shipBuildingApplicable ?? false, - windApplicable: template?.windApplicable ?? false, - pcApplicable: template?.pcApplicable ?? false, - nbApplicable: template?.nbApplicable ?? false, - rcApplicable: template?.rcApplicable ?? false, - gyApplicable: template?.gyApplicable ?? false, - sysApplicable: template?.sysApplicable ?? false, - infraApplicable: template?.infraApplicable ?? false, }, mode: "onChange" }) @@ -145,52 +87,23 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem } }; - // 모든 적용 범위 선택/해제 - const handleSelectAllScopes = (checked: boolean | "indeterminate") => { - const value = checked === true; - BUSINESS_UNITS.forEach(unit => { - form.setValue(unit.key as keyof UpdateTemplateSchema, value); - }); - }; - // 템플릿 변경 시 폼 값 업데이트 React.useEffect(() => { if (template) { form.reset({ - templateName: template.templateName as typeof TEMPLATE_NAME_OPTIONS[number], legalReviewRequired: template.legalReviewRequired ?? false, - shipBuildingApplicable: template.shipBuildingApplicable ?? false, - windApplicable: template.windApplicable ?? false, - pcApplicable: template.pcApplicable ?? false, - nbApplicable: template.nbApplicable ?? false, - rcApplicable: template.rcApplicable ?? false, - gyApplicable: template.gyApplicable ?? false, - sysApplicable: template.sysApplicable ?? false, - infraApplicable: template.infraApplicable ?? false, }); } }, [template, form]); - // 현재 선택된 적용 범위 수 - const selectedScopesCount = BUSINESS_UNITS.filter(unit => - form.watch(unit.key as keyof UpdateTemplateSchema) - ).length; - function onSubmit(input: UpdateTemplateSchema) { startUpdateTransition(async () => { if (!template) return // FormData 객체 생성하여 파일과 데이터를 함께 전송 const formData = new FormData(); - formData.append("templateName", input.templateName); formData.append("legalReviewRequired", input.legalReviewRequired.toString()); - // 적용 범위 추가 - BUSINESS_UNITS.forEach(unit => { - const value = input[unit.key as keyof UpdateTemplateSchema] as boolean; - formData.append(unit.key, value.toString()); - }); - if (input.file) { formData.append("file", input.file); } @@ -221,24 +134,14 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem if (!template) return null; - const scopeSelected = BUSINESS_UNITS.some( - (unit) => form.watch(unit.key as keyof UpdateTemplateSchema) - ); - - const isDisabled = - isUpdatePending || - !form.watch("templateName") || - !scopeSelected; - return ( - + {/* 고정된 헤더 */} 템플릿 업데이트 - 템플릿 정보를 수정하고 변경사항을 저장하세요 - * 표시된 항목은 필수 입력사항입니다. + 템플릿 파일을 업데이트하고 설정을 변경하세요 @@ -249,51 +152,49 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-4" > - {/* 기본 정보 */} + {/* 템플릿 정보 표시 */} - 기본 정보 + 템플릿 정보 - 현재 리비전: v{template.revision} -
- 현재 적용 범위: {scopeHelpers.getScopeDisplayText(template)} + 현재 템플릿의 기본 정보입니다
- ( - - - 템플릿 이름 * - - - - 미리 정의된 템플릿 중에서 선택 - - - - )} - /> +
+ +
+ {template.templateName} +
+
+ +
+ +
+ v{template.revision} +
+
+ +
+ +
+ {template.fileName} +
+
+
+
+ {/* 설정 */} + + + 설정 + + 템플릿 관련 설정을 변경할 수 있습니다 + + + - {/* 적용 범위 */} - - - - 적용 범위 * - - - 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨) - - - -
- - -
- - - -
- {BUSINESS_UNITS.map((unit) => ( - ( - - - - -
- - {unit.label} - -
-
- )} - /> - ))} -
- - {form.formState.errors.shipBuildingApplicable && ( -

- {form.formState.errors.shipBuildingApplicable.message} -

- )} -
-
- {/* 파일 업데이트 */} 파일 업데이트 - 현재 파일: {template.fileName} + 새로운 템플릿 파일을 업로드하세요 @@ -388,7 +232,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem name="file" render={() => ( - 템플릿 파일 (선택사항) + 새 템플릿 파일 (선택사항) {selectedFile ? selectedFile.name - : "새 워드 파일을 드래그하세요 (선택사항)"} + : "새 워드 파일을 드래그하세요"} {selectedFile @@ -413,6 +257,9 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem + + 파일을 업로드하지 않으면 기존 파일이 유지됩니다 + )} @@ -433,12 +280,12 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
diff --git a/lib/basic-contract/validations.ts b/lib/basic-contract/validations.ts index e8b28e73..bb9e3b8d 100644 --- a/lib/basic-contract/validations.ts +++ b/lib/basic-contract/validations.ts @@ -65,16 +65,7 @@ export const BUSINESS_UNIT_KEYS = [ export const createBasicContractTemplateSchema = z.object({ templateName: z.string().min(1, "템플릿 이름은 필수입니다."), legalReviewRequired: z.boolean().default(false), - // 적용 범위 - shipBuildingApplicable: z.boolean().default(false), - windApplicable: z.boolean().default(false), - pcApplicable: z.boolean().default(false), - nbApplicable: z.boolean().default(false), - rcApplicable: z.boolean().default(false), - gyApplicable: z.boolean().default(false), - sysApplicable: z.boolean().default(false), - infraApplicable: z.boolean().default(false), - + status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), fileName: z.string().nullable().optional(), filePath: z.string().nullable().optional(), @@ -82,12 +73,6 @@ export const createBasicContractTemplateSchema = z.object({ // 기존에 쓰시던 validityPeriod 를 계속 쓰실 거라면 남기고, 아니라면 지우세요. // 예: 문자열(YYYY-MM-DD ~ YYYY-MM-DD) 또는 number(개월 수) 등 구체화 필요 validityPeriod: z.string().optional(), -}).refine((data) => { - // 최소 1개 이상 사업부 선택 - return BUSINESS_UNIT_KEYS.some((k) => data[k] === true); -}, { - message: "적어도 하나의 적용 범위를 선택해야 합니다.", - path: ["shipBuildingApplicable"], // 첫 체크박스에 에러 표시 유도 }); export type CreateBasicContractTemplateSchema = z.infer; diff --git a/lib/basic-contract/vendor-table/basic-contract-columns.tsx b/lib/basic-contract/vendor-table/basic-contract-columns.tsx index c9e8da53..1b11285c 100644 --- a/lib/basic-contract/vendor-table/basic-contract-columns.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-columns.tsx @@ -32,14 +32,65 @@ import { BasicContractView } from "@/db/schema" interface GetColumnsProps { setRowAction: React.Dispatch | null>> + locale?: string + t: (key: string) => string // 번역 함수 } +// 기본 번역값들 (fallback) +const fallbackTranslations = { + ko: { + download: "다운로드", + selectAll: "전체 선택", + selectRow: "행 선택", + fileInfoMissing: "파일 정보가 없습니다.", + fileDownloadError: "파일 다운로드 중 오류가 발생했습니다.", + statusValues: { + PENDING: "서명대기", + COMPLETED: "서명완료" + } + }, + en: { + download: "Download", + selectAll: "Select all", + selectRow: "Select row", + fileInfoMissing: "File information is missing.", + fileDownloadError: "An error occurred while downloading the file.", + statusValues: { + PENDING: "Pending", + COMPLETED: "Completed" + } + } +}; + +// 안전한 번역 함수 +const safeTranslate = (t: (key: string) => string, key: string, locale: string = 'ko', fallback?: string): string => { + try { + const translated = t(key); + // 번역 키가 그대로 반환되는 경우 (번역 실패) fallback 사용 + if (translated === key && fallback) { + return fallback; + } + return translated || fallback || key; + } catch (error) { + console.warn(`Translation failed for key: ${key}`, error); + return fallback || key; + } +}; + /** * 파일 다운로드 함수 */ -const handleFileDownload = async (filePath: string | null, fileName: string | null) => { +const handleFileDownload = async ( + filePath: string | null, + fileName: string | null, + t: (key: string) => string, + locale: string = 'ko' +) => { + const fallback = fallbackTranslations[locale as keyof typeof fallbackTranslations] || fallbackTranslations.ko; + if (!filePath || !fileName) { - toast.error("파일 정보가 없습니다."); + const message = safeTranslate(t, "basicContracts.fileInfoMissing", locale, fallback.fileInfoMissing); + toast.error(message); return; } @@ -57,14 +108,17 @@ const handleFileDownload = async (filePath: string | null, fileName: string | nu } } catch (error) { console.error("파일 다운로드 오류:", error); - toast.error("파일 다운로드 중 오류가 발생했습니다."); + const message = safeTranslate(t, "basicContracts.fileDownloadError", locale, fallback.fileDownloadError); + toast.error(message); } }; /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ -export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { +export function getColumns({ setRowAction, locale = 'ko', t }: GetColumnsProps): ColumnDef[] { + const fallback = fallbackTranslations[locale as keyof typeof fallbackTranslations] || fallbackTranslations.ko; + // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- @@ -77,7 +131,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" + aria-label={safeTranslate(t, "basicContracts.selectAll", locale, fallback.selectAll)} className="translate-y-0.5" /> ), @@ -85,7 +139,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef row.toggleSelected(!!value)} - aria-label="Select row" + aria-label={safeTranslate(t, "basicContracts.selectRow", locale, fallback.selectRow)} className="translate-y-0.5" /> ), @@ -105,18 +159,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef handleFileDownload(filePath, fileName)} - title={`${fileName} 다운로드`} + onClick={() => handleFileDownload(filePath, fileName, t, locale)} + title={`${fileName} ${downloadText}`} className="hover:bg-muted" disabled={!filePath || !fileName} > - 다운로드 + {downloadText} ); }, @@ -124,7 +179,6 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef { - // 날짜 형식 처리 + // 날짜 형식 처리 - 로케일 적용 if (cfg.id === "createdAt" || cfg.id === "updatedAt" || cfg.id === "completedAt") { const dateVal = cell.getValue() as Date - return formatDateTime(dateVal) + return formatDateTime(dateVal, locale) } - // Status 컬럼에 Badge 적용 + // Status 컬럼에 Badge 적용 - 다국어 적용 if (cfg.id === "status") { const status = row.getValue(cfg.id) as string const isPending = status === "PENDING" + const statusText = safeTranslate( + t, + `basicContracts.statusValues.${status}`, + locale, + fallback.statusValues[status as keyof typeof fallback.statusValues] || status + ); return ( - {status} + {statusText} ) } @@ -175,8 +235,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef void; + hasSelectedRows?: boolean; + t: (key: string) => string; } -export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractSignDialogProps) { +export function BasicContractSignDialog({ + contracts, + onSuccess, + hasSelectedRows = false, + t +}: BasicContractSignDialogProps) { const [open, setOpen] = React.useState(false); const [selectedContract, setSelectedContract] = React.useState(null); const [instance, setInstance] = React.useState(null); const [searchTerm, setSearchTerm] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); + + // 추가된 state들 + const [additionalFiles, setAdditionalFiles] = React.useState([]); + const [isLoadingAttachments, setIsLoadingAttachments] = React.useState(false); + const router = useRouter() + console.log(selectedContract,"selectedContract") + console.log(additionalFiles,"additionalFiles") + + // 버튼 비활성화 조건 + const isButtonDisabled = !hasSelectedRows || contracts.length === 0; + + // 비활성화 이유 텍스트 + const getDisabledReason = () => { + if (!hasSelectedRows) { + return t("basicContracts.toolbar.selectRows"); + } + if (contracts.length === 0) { + return t("basicContracts.toolbar.noPendingContracts"); + } + return ""; + }; + // 다이얼로그 열기/닫기 핸들러 const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen); - // 다이얼로그가 열릴 때 첫 번째 계약서 자동 선택 - if (isOpen && contracts.length > 0 && !selectedContract) { - setSelectedContract(contracts[0]); - } - if (!isOpen) { + // 다이얼로그 닫을 때 상태 초기화 setSelectedContract(null); setSearchTerm(""); + setAdditionalFiles([]); // 추가 파일 상태 초기화 + // WebViewer 인스턴스 정리 + if (instance) { + try { + instance.UI.dispose(); + } catch (error) { + console.log("WebViewer dispose error:", error); + } + setInstance(null); + } } }; // 계약서 선택 핸들러 const handleSelectContract = (contract: BasicContractView) => { + console.log("계약서 선택:", contract.id, contract.templateName); setSelectedContract(contract); }; @@ -79,6 +115,40 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS } }, [open, contracts, selectedContract]); + // 추가 파일 가져오기 useEffect + React.useEffect(() => { + const fetchAdditionalFiles = async () => { + if (!selectedContract) { + setAdditionalFiles([]); + return; + } + + // "비밀유지 계약서"인 경우에만 추가 파일 가져오기 + if (selectedContract.templateName === "비밀유지 계약서") { + setIsLoadingAttachments(true); + try { + const result = await getVendorAttachments(selectedContract.vendorId); + if (result.success) { + setAdditionalFiles(result.data); + console.log("추가 파일 로드됨:", result.data); + } else { + console.error("Failed to fetch attachments:", result.error); + setAdditionalFiles([]); + } + } catch (error) { + console.error("Error fetching attachments:", error); + setAdditionalFiles([]); + } finally { + setIsLoadingAttachments(false); + } + } else { + setAdditionalFiles([]); + } + }; + + fetchAdditionalFiles(); + }, [selectedContract]); + // 서명 완료 핸들러 const completeSign = async () => { if (!instance || !selectedContract) return; @@ -89,29 +159,57 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS const doc = documentViewer.getDocument(); const xfdfString = await annotationManager.exportAnnotations(); + // 폼 필드 데이터 수집 + const fieldManager = annotationManager.getFieldManager(); + const fields = fieldManager.getFields(); + const formData: any = {}; + fields.forEach((field: any) => { + formData[field.name] = field.value; + }); + const data = await doc.getFileData({ xfdfString, downloadType: "pdf", }); // FormData 생성 및 파일 추가 - const formData = new FormData(); - formData.append('file', new Blob([data], { type: 'application/pdf' })); - formData.append('tableRowId', selectedContract.id.toString()); - formData.append('templateName', selectedContract.signedFileName || ''); + const submitFormData = new FormData(); + submitFormData.append('file', new Blob([data], { type: 'application/pdf' })); + submitFormData.append('tableRowId', selectedContract.id.toString()); + submitFormData.append('templateName', selectedContract.signedFileName || ''); + + // 폼 필드 데이터 추가 + if (Object.keys(formData).length > 0) { + submitFormData.append('formData', JSON.stringify(formData)); + } + + // 준법 템플릿인 경우 필수 필드 검증 + if (selectedContract.templateName?.includes('준법')) { + const requiredFields = ['compliance_agreement', 'legal_review', 'risk_assessment']; + const missingFields = requiredFields.filter(field => !formData[field]); + + if (missingFields.length > 0) { + toast.error("필수 준법 항목이 누락되었습니다.", { + description: `다음 항목을 완료해주세요: ${missingFields.join(', ')}`, + icon: + }); + setIsSubmitting(false); + return; + } + } // API 호출 const response = await fetch('/api/upload/signed-contract', { method: 'POST', - body: formData, + body: submitFormData, next: { tags: ["basicContractView-vendor"] }, }); const result = await response.json(); if (result.result) { - toast.success("서명이 성공적으로 완료되었습니다.", { - description: "문서가 성공적으로 처리되었습니다.", + toast.success(t("basicContracts.messages.signSuccess"), { + description: t("basicContracts.messages.documentProcessed"), icon: }); router.refresh(); @@ -120,22 +218,19 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS onSuccess(); } } else { - toast.error("서명 처리 중 오류가 발생했습니다.", { + toast.error(t("basicContracts.messages.signError"), { description: result.error, icon: }); } } catch (error) { console.error("서명 완료 중 오류:", error); - toast.error("서명 처리 중 오류가 발생했습니다."); + toast.error(t("basicContracts.messages.signError")); } finally { setIsSubmitting(false); } }; - // 서명 대기중(PENDING) 계약서가 있는지 확인 - const hasPendingContracts = contracts.length > 0; - return ( <> {/* 서명 버튼 */} @@ -143,62 +238,67 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS variant="outline" size="sm" onClick={() => setOpen(true)} - disabled={!hasPendingContracts} - className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200" + disabled={isButtonDisabled} + className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed" > -