From 1dc24d48e52f2e490f5603ceb02842586ecae533 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 24 Jul 2025 11:06:32 +0000 Subject: (대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업 피드백 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/basic-contract/service.ts | 230 ++++++-- .../add-basic-contract-template-dialog.tsx | 268 ++++----- .../template/basic-contract-template-columns.tsx | 41 +- .../template/basic-contract-template.tsx | 13 +- .../template/create-revision-dialog.tsx | 564 +++++++++++++++++++ .../template/update-basicContract-sheet.tsx | 106 ++-- lib/basic-contract/validations.ts | 42 +- lib/evaluation-target-list/service.ts | 267 +++++---- .../table/delete-targets-dialog.tsx | 181 ++++++ .../table/evaluation-target-table.tsx | 199 +++++-- .../table/evaluation-targets-columns.tsx | 87 ++- .../table/evaluation-targets-filter-sheet.tsx | 304 ++++------ .../table/evaluation-targets-toolbar-actions.tsx | 32 +- .../manual-create-evaluation-target-dialog.tsx | 2 +- .../table/update-evaluation-target.tsx | 4 +- lib/evaluation-target-list/validation.ts | 57 +- lib/evaluation/service.ts | 214 +++++-- lib/evaluation/table/evaluation-columns.tsx | 455 +++++++++++---- lib/evaluation/table/evaluation-filter-sheet.tsx | 616 +++++++-------------- lib/evaluation/table/evaluation-table.tsx | 341 ++++++++---- lib/evaluation/table/evaluation-view-toggle.tsx | 88 +++ lib/evaluation/validation.ts | 13 +- lib/gtc-contract/service.ts | 363 ++++++++++++ .../status/create-gtc-document-dialog.tsx | 272 +++++++++ .../status/create-new-revision-dialog.tsx | 157 ++++++ .../status/delete-gtc-documents-dialog.tsx | 168 ++++++ lib/gtc-contract/status/gtc-contract-table.tsx | 173 ++++++ .../status/gtc-documents-table-columns.tsx | 291 ++++++++++ .../status/gtc-documents-table-floating-bar.tsx | 90 +++ .../status/gtc-documents-table-toolbar-actions.tsx | 39 ++ .../status/update-gtc-document-sheet.tsx | 148 +++++ lib/gtc-contract/validations.ts | 89 +++ lib/items-tech/service.ts | 42 +- lib/items-tech/table/add-items-dialog.tsx | 38 +- .../table/hull/offshore-hull-table-columns.tsx | 2 +- lib/items-tech/table/top/import-item-handler.tsx | 4 +- .../table/top/offshore-top-table-columns.tsx | 2 +- .../top/offshore-top-table-toolbar-actions.tsx | 2 +- lib/items-tech/table/top/offshore-top-table.tsx | 3 +- lib/items-tech/table/update-items-sheet.tsx | 56 +- lib/items-tech/validations.ts | 6 +- lib/tech-vendors/service.ts | 14 +- lib/techsales-rfq/service.ts | 33 +- .../detail/quotation-response-tab.tsx | 7 +- .../table/esg-evaluation-form-sheet.tsx | 69 ++- 45 files changed, 4795 insertions(+), 1397 deletions(-) create mode 100644 lib/basic-contract/template/create-revision-dialog.tsx create mode 100644 lib/evaluation-target-list/table/delete-targets-dialog.tsx create mode 100644 lib/evaluation/table/evaluation-view-toggle.tsx create mode 100644 lib/gtc-contract/service.ts create mode 100644 lib/gtc-contract/status/create-gtc-document-dialog.tsx create mode 100644 lib/gtc-contract/status/create-new-revision-dialog.tsx create mode 100644 lib/gtc-contract/status/delete-gtc-documents-dialog.tsx create mode 100644 lib/gtc-contract/status/gtc-contract-table.tsx create mode 100644 lib/gtc-contract/status/gtc-documents-table-columns.tsx create mode 100644 lib/gtc-contract/status/gtc-documents-table-floating-bar.tsx create mode 100644 lib/gtc-contract/status/gtc-documents-table-toolbar-actions.tsx create mode 100644 lib/gtc-contract/status/update-gtc-document-sheet.tsx create mode 100644 lib/gtc-contract/validations.ts (limited to 'lib') diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 87a861e1..014f32ab 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -49,8 +49,6 @@ export async function addTemplate( if (templateData instanceof FormData) { const templateName = templateData.get("templateName") as string; // 문자열을 숫자로 변환 (FormData의 get은 항상 string|File을 반환) - const validityPeriodStr = templateData.get("validityPeriod") as string; - const validityPeriod = validityPeriodStr ? parseInt(validityPeriodStr, 10) : 12; // 기본값 12개월 const status = templateData.get("status") as "ACTIVE" | "INACTIVE"; const file = templateData.get("file") as File; @@ -63,12 +61,6 @@ export async function addTemplate( return { success: false, error: "파일은 필수입니다." }; } - if (isNaN(validityPeriod) || validityPeriod < 1 || validityPeriod > 120) { - return { - success: false, - error: "유효기간은 1~120개월 사이의 유효한 값이어야 합니다." - }; - } const saveResult = await saveFile({file, directory:"basicContract/template" }); if (!saveResult.success) { @@ -79,7 +71,6 @@ export async function addTemplate( const formattedData = { templateName, status, - validityPeriod, // 숫자로 변환된 유효기간 fileName: file.name, filePath: saveResult.publicPath! }; @@ -196,26 +187,31 @@ export async function getBasicContractTemplates( // 템플릿 생성 (서버 액션) -export async function createBasicContractTemplate( - input: CreateBasicContractTemplateSchema -) { +export async function createBasicContractTemplate(input: CreateBasicContractTemplateSchema) { unstable_noStore(); + try { const newTemplate = await db.transaction(async (tx) => { - const [newTemplate] = await insertBasicContractTemplate(tx, { + const [row] = await insertBasicContractTemplate(tx, { templateName: input.templateName, - validityPeriod: input.validityPeriod, + 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, fileName: input.fileName, filePath: input.filePath, + // 필요하면 createdAt/updatedAt 등도 여기서 }); - return newTemplate; + return row; }); - // 캐시 무효화 - revalidateTag("basic-contract-templates"); - revalidateTag("template-status-counts"); - return { data: newTemplate, error: null }; } catch (error) { return { data: null, error: getErrorMessage(error) }; @@ -350,6 +346,23 @@ interface UpdateTemplateParams { formData: FormData; } +const SCOPE_KEYS = [ + "shipBuildingApplicable", + "windApplicable", + "pcApplicable", + "nbApplicable", + "rcApplicable", + "gyApplicable", + "sysApplicable", + "infraApplicable", +] as const; + +function getBool(fd: FormData, key: string, defaultValue = false) { + const v = fd.get(key); + if (v === null) return defaultValue; + return v === "true"; +} + export async function updateTemplate({ id, formData @@ -357,51 +370,76 @@ export async function updateTemplate({ unstable_noStore(); try { - const templateName = formData.get("templateName") as string; - const validityPeriodStr = formData.get("validityPeriod") as string; - const validityPeriod = validityPeriodStr ? parseInt(validityPeriodStr, 10) : 12; - const status = formData.get("status") as "ACTIVE" | "INACTIVE"; - const file = formData.get("file") as File | null; - + // 필수값 + const templateName = formData.get("templateName") as string | null; if (!templateName) { return { error: "템플릿 이름은 필수입니다." }; } - // 기본 업데이트 데이터 - const updateData: Record = { - templateName, - status, - validityPeriod, - updatedAt: new Date(), - }; + // 선택/추가 필드 파싱 + const revisionStr = formData.get("revision")?.toString() ?? "1"; + const revision = Number(revisionStr) || 1; + + const legalReviewRequired = getBool(formData, "legalReviewRequired", false); + + // status는 프런트에서 ACTIVE만 넣고 있으나, 없으면 기존값 유지 or 기본값 설정 + const status = (formData.get("status") as "ACTIVE" | "INACTIVE" | null) ?? "ACTIVE"; + // validityPeriod가 이제 필요없다면 제거하시고, 사용한다면 파싱 그대로 + const validityPeriodStr = formData.get("validityPeriod")?.toString(); + const validityPeriod = validityPeriodStr ? Number(validityPeriodStr) : undefined; + + // Scope booleans + const scopeData: Record = {}; + for (const key of SCOPE_KEYS) { + scopeData[key] = getBool(formData, key, false); + } + + // 파일 처리 + const file = formData.get("file") as File | null; + let fileName: string | undefined = undefined; + let filePath: string | undefined = undefined; - // 파일이 있는 경우 처리 if (file) { - const saveResult = await saveFile({file,directory:"basicContract/template"}); + // 1) 새 파일 저장 + const saveResult = await saveFile({ file, directory: "basicContract/template" }); if (!saveResult.success) { return { success: false, error: saveResult.error }; } + fileName = file.name; + filePath = saveResult.publicPath; - // 기존 파일 정보 가져오기 + // 2) 기존 파일 삭제 const existingTemplate = await db.query.basicContractTemplates.findFirst({ - where: eq(basicContractTemplates.id, id) + where: eq(basicContractTemplates.id, id), }); - // 기존 파일이 있다면 삭제 if (existingTemplate?.filePath) { - const deleted = await deleteFile(existingTemplate.filePath); if (deleted) { - console.log(`✅ 파일 삭제됨: ${existingTemplate.filePath}`); + console.log(`✅ 기존 파일 삭제됨: ${existingTemplate.filePath}`); } else { - console.log(`⚠️ 파일 삭제 실패: ${existingTemplate.filePath}`); + console.log(`⚠️ 기존 파일 삭제 실패: ${existingTemplate.filePath}`); } } + } - // 업데이트 데이터에 파일 정보 추가 - updateData.fileName = file.name; - updateData.filePath = saveResult.publicPath; + // 업데이트할 데이터 구성 + const updateData: Record = { + templateName, + revision, + legalReviewRequired, + status, + updatedAt: new Date(), + ...scopeData, + }; + + if (validityPeriod !== undefined) { + updateData.validityPeriod = validityPeriod; + } + if (fileName && filePath) { + updateData.fileName = fileName; + updateData.filePath = filePath; } // DB 업데이트 @@ -412,7 +450,7 @@ export async function updateTemplate({ .where(eq(basicContractTemplates.id, id)); }); - // 캐시 무효화 (다양한 방법 시도) + // 캐시 무효화 revalidateTag("basic-contract-templates"); revalidateTag("template-status-counts"); revalidateTag("templates"); @@ -423,7 +461,7 @@ export async function updateTemplate({ return { error: error instanceof Error ? error.message - : "템플릿 업데이트 중 오류가 발생했습니다." + : "템플릿 업데이트 중 오류가 발생했습니다.", }; } } @@ -987,4 +1025,106 @@ export async function saveTemplateFile(templateId: number, formData: FormData) { export async function refreshTemplatePage(templateId: string) { revalidatePath(`/evcp/basic-contract-template/${templateId}`); revalidateTag("basic-contract-templates"); +} + +// 새 리비전 생성 함수 +export async function createBasicContractTemplateRevision(input: CreateRevisionSchema) { + unstable_noStore(); + + try { + // 기본 템플릿 존재 확인 + const baseTemplate = await db + .select() + .from(basicContractTemplates) + .where(eq(basicContractTemplates.id, input.baseTemplateId)) + .limit(1); + + if (baseTemplate.length === 0) { + return { data: null, error: "기본 템플릿을 찾을 수 없습니다." }; + } + + // 같은 템플릿 이름에 해당 리비전이 이미 존재하는지 확인 + const existingRevision = await db + .select() + .from(basicContractTemplates) + .where( + and( + eq(basicContractTemplates.templateName, input.templateName), + eq(basicContractTemplates.revision, input.revision) + ) + ) + .limit(1); + + if (existingRevision.length > 0) { + return { + data: null, + error: `${input.templateName} v${input.revision} 리비전이 이미 존재합니다.` + }; + } + + // 새 리비전이 기존 리비전들보다 큰 번호인지 확인 + const maxRevision = await db + .select({ maxRev: basicContractTemplates.revision }) + .from(basicContractTemplates) + .where(eq(basicContractTemplates.templateName, input.templateName)) + .orderBy(desc(basicContractTemplates.revision)) + .limit(1); + + if (maxRevision.length > 0 && input.revision <= maxRevision[0].maxRev) { + return { + data: null, + error: `새 리비전 번호는 현재 최대 리비전(v${maxRevision[0].maxRev})보다 커야 합니다.` + }; + } + + const newRevision = await db.transaction(async (tx) => { + const [row] = await insertBasicContractTemplate(tx, { + templateName: input.templateName, + revision: input.revision, + 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: "ACTIVE", + fileName: input.fileName, + filePath: input.filePath, + validityPeriod: null, + }); + return row; + }); + + return { data: newRevision, error: null }; + } catch (error) { + return { data: null, error: getErrorMessage(error) }; + } +} + + + + +// 1) 전체 basicContractTemplates 조회 +export async function getALLBasicContractTemplates() { + return db + .select() + .from(basicContractTemplates) + .where(eq(basicContractTemplates.status,"ACTIVE")) + .orderBy(desc(basicContractTemplates.createdAt)); +} + +// 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); } \ No newline at end of file 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 9b036445..fd1bd333 100644 --- a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -40,10 +40,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Separator } from "@/components/ui/separator"; import { useRouter } from "next/navigation"; import { BUSINESS_UNITS } from "@/config/basicContractColumnsConfig"; +import { getExistingTemplateNames } from "../service"; -// 템플릿 이름 옵션 정의 +// ✅ 서버 액션 import + +// 전체 템플릿 후보 const TEMPLATE_NAME_OPTIONS = [ - "준법서약", + "준법서약 (한글)", + "준법서약 (영문)", "기술자료 요구서", "비밀유지 계약서", "표준하도급기본 계약서", @@ -56,14 +60,11 @@ const TEMPLATE_NAME_OPTIONS = [ "직납자재 하도급대급등 연동제 의향서" ] as const; -// 업데이트된 계약서 템플릿 스키마 정의 (워드파일만 허용) const templateFormSchema = z.object({ templateName: z.enum(TEMPLATE_NAME_OPTIONS, { required_error: "템플릿 이름을 선택해주세요.", }), - revision: z.coerce.number().int().min(1).default(1), legalReviewRequired: z.boolean().default(false), - // 적용 범위 shipBuildingApplicable: z.boolean().default(false), windApplicable: z.boolean().default(false), @@ -73,31 +74,42 @@ const templateFormSchema = z.object({ gyApplicable: z.boolean().default(false), sysApplicable: z.boolean().default(false), infraApplicable: z.boolean().default(false), - - file: z - .instanceof(File, { message: "파일을 업로드해주세요." }) - .refine((file) => file.size <= 100 * 1024 * 1024, { - message: "파일 크기는 100MB 이하여야 합니다.", - }) - .refine( - (file) => - file.type === 'application/msword' || - file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - { message: "워드 파일(.doc, .docx)만 업로드 가능합니다." } - ), - status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), -}).refine((data) => { - // 적어도 하나의 적용 범위는 선택되어야 함 + file: z.instanceof(File).optional(), +}) +.refine((data) => { + if (data.templateName !== "General GTC" && !data.file) return false; + return true; +}, { + message: "파일을 업로드해주세요.", + path: ["file"], +}) +.refine((data) => { + if (data.file && data.file.size > 100 * 1024 * 1024) return false; + return true; +}, { + message: "파일 크기는 100MB 이하여야 합니다.", + path: ["file"], +}) +.refine((data) => { + if (data.file) { + const isValidType = data.file.type === 'application/msword' || + data.file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + return isValidType; + } + return true; +}, { + message: "워드 파일(.doc, .docx)만 업로드 가능합니다.", + path: ["file"], +}) +.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; + return scopeFields.some(field => data[field as keyof typeof data] === true); }, { message: "적어도 하나의 적용 범위를 선택해야 합니다.", - path: ["shipBuildingApplicable"], // 에러를 첫 번째 체크박스에 표시 + path: ["shipBuildingApplicable"], }); type TemplateFormValues = z.infer; @@ -108,12 +120,12 @@ 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 router = useRouter(); - // 기본값 설정 (templateCode 제거) + // 기본값 const defaultValues: Partial = { templateName: undefined, - revision: 1, legalReviewRequired: false, shipBuildingApplicable: false, windApplicable: false, @@ -123,17 +135,33 @@ export function AddTemplateDialog() { gyApplicable: false, sysApplicable: false, infraApplicable: false, - status: "ACTIVE", }; - // 폼 초기화 const form = useForm({ resolver: zodResolver(templateFormSchema), defaultValues, mode: "onChange", }); - // 파일 선택 핸들러 + // 🔸 마운트 시 이미 등록된 templateName 목록 가져와서 필터링 + React.useEffect(() => { + let cancelled = false; + (async () => { + try { + const usedNames = await getExistingTemplateNames(); + if (cancelled) return; + + // 이미 있는 이름 제외 + const filtered = TEMPLATE_NAME_OPTIONS.filter(name => !usedNames.includes(name)); + setAvailableTemplateNames(filtered); + } catch (err) { + console.error("Failed to fetch existing template names", err); + // 실패 시 전체 옵션 보여주거나, 오류 알려주기 + } + })(); + return () => { cancelled = true; }; + }, []); + const handleFileChange = (files: File[]) => { if (files.length > 0) { const file = files[0]; @@ -142,88 +170,71 @@ export function AddTemplateDialog() { } }; - // 모든 적용 범위 선택/해제 const handleSelectAllScopes = (checked: boolean) => { BUSINESS_UNITS.forEach(unit => { form.setValue(unit.key as keyof TemplateFormValues, checked); }); }; - // 청크 크기 설정 (1MB) + // 청크 업로드 설정 const CHUNK_SIZE = 1 * 1024 * 1024; - // 파일을 청크로 분할하여 업로드하는 함수 const uploadFileInChunks = async (file: File, fileId: string) => { const totalChunks = Math.ceil(file.size / CHUNK_SIZE); setShowProgress(true); setUploadProgress(0); - + for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const start = chunkIndex * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); - + const formData = new FormData(); formData.append('chunk', chunk); formData.append('filename', file.name); formData.append('chunkIndex', chunkIndex.toString()); formData.append('totalChunks', totalChunks.toString()); formData.append('fileId', fileId); - - try { - const response = await fetch('/api/upload/basicContract/chunk', { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - throw new Error(`청크 업로드 실패: ${response.statusText}`); - } - - // 진행률 업데이트 - const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100); - setUploadProgress(progress); - - const result = await response.json(); - - // 마지막 청크인 경우 파일 경로 반환 - if (chunkIndex === totalChunks - 1) { - return result; - } - } catch (error) { - console.error(`청크 ${chunkIndex} 업로드 오류:`, error); - throw error; + + const response = await fetch('/api/upload/basicContract/chunk', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`청크 업로드 실패: ${response.statusText}`); + } + + const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100); + setUploadProgress(progress); + + const result = await response.json(); + if (chunkIndex === totalChunks - 1) { + return result; } } }; - // 폼 제출 핸들러 (templateCode 제거) async function onSubmit(formData: TemplateFormValues) { setIsLoading(true); try { - if (!formData.file) { - throw new Error("파일이 선택되지 않았습니다."); - } - - // 고유 파일 ID 생성 - const fileId = uuidv4(); - - // 파일 청크 업로드 - const uploadResult = await uploadFileInChunks(formData.file, fileId); - - if (!uploadResult.success) { - throw new Error("파일 업로드에 실패했습니다."); + let uploadResult = null; + + if (formData.file) { + const fileId = uuidv4(); + uploadResult = await uploadFileInChunks(formData.file, fileId); + + if (!uploadResult?.success) { + throw new Error("파일 업로드에 실패했습니다."); + } } - - // 메타데이터 저장 (templateCode 제거됨) + const saveResponse = await fetch('/api/upload/basicContract/complete', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ templateName: formData.templateName, - revision: formData.revision, + revision: 1, legalReviewRequired: formData.legalReviewRequired, shipBuildingApplicable: formData.shipBuildingApplicable, windApplicable: formData.windApplicable, @@ -233,35 +244,32 @@ export function AddTemplateDialog() { gyApplicable: formData.gyApplicable, sysApplicable: formData.sysApplicable, infraApplicable: formData.infraApplicable, - status: formData.status, - fileName: uploadResult.fileName, - filePath: uploadResult.filePath, + status: "ACTIVE", + fileName: uploadResult?.fileName || `${formData.templateName}_v1.docx`, + filePath: uploadResult?.filePath || "", }), next: { tags: ["basic-contract-templates"] }, }); - + const saveResult = await saveResponse.json(); - if (!saveResult.success) { - throw new Error("템플릿 정보 저장에 실패했습니다."); + throw new Error(saveResult.error || "템플릿 정보 저장에 실패했습니다."); } - + toast.success('템플릿이 성공적으로 추가되었습니다.'); form.reset(); setSelectedFile(null); setOpen(false); setShowProgress(false); - router.refresh(); } catch (error) { console.error("Submit error:", error); - toast.error("템플릿 추가 중 오류가 발생했습니다."); + toast.error(error instanceof Error ? error.message : "템플릿 추가 중 오류가 발생했습니다."); } finally { setIsLoading(false); } } - // 모달이 닫힐 때 폼 초기화 React.useEffect(() => { if (!open) { form.reset(); @@ -278,11 +286,17 @@ export function AddTemplateDialog() { setOpen(nextOpen); } - // 현재 선택된 적용 범위 수 - const selectedScopesCount = BUSINESS_UNITS.filter(unit => + const selectedScopesCount = BUSINESS_UNITS.filter(unit => form.watch(unit.key as keyof TemplateFormValues) ).length; + const templateNameIsRequired = form.watch("templateName") !== "General GTC"; + + const isSubmitDisabled = isLoading || + !form.watch("templateName") || + (templateNameIsRequired && !form.watch("file")) || + !BUSINESS_UNITS.some(unit => form.watch(unit.key as keyof TemplateFormValues)); + return ( @@ -291,16 +305,14 @@ export function AddTemplateDialog() { - {/* 고정된 헤더 */} 새 기본계약서 템플릿 추가 - 템플릿 정보를 입력하고 계약서 파일을 업로드하세요. + 템플릿 정보를 입력하고 계약서 파일을 업로드하세요. (리비전은 자동으로 1로 설정됩니다) * 표시된 항목은 필수 입력사항입니다. - {/* 스크롤 가능한 컨텐츠 영역 */}
@@ -310,7 +322,7 @@ export function AddTemplateDialog() { 기본 정보 -
+
템플릿 이름 * - - - + + - {TEMPLATE_NAME_OPTIONS.map((option) => ( + {availableTemplateNames.map((option) => ( {option} @@ -334,33 +350,7 @@ export function AddTemplateDialog() { - 미리 정의된 템플릿 중에서 선택 - - - - )} - /> - - ( - - 리비전 - - field.onChange(parseInt(e.target.value) || 1)} - /> - - - 템플릿 버전 (기본값: 1) -
- - 동일한 템플릿 이름의 리비전은 중복될 수 없습니다. - + 이미 등록되지 않은 템플릿만 표시됩니다. (리비전 1로 생성)
@@ -412,9 +402,9 @@ export function AddTemplateDialog() { 전체 선택
- + - +
{BUSINESS_UNITS.map((unit) => ( ))}
- + {form.formState.errors.shipBuildingApplicable && (

{form.formState.errors.shipBuildingApplicable.message} @@ -452,6 +442,11 @@ export function AddTemplateDialog() { 파일 업로드 + + {form.watch("templateName") === "General GTC" + ? "General GTC는 파일 업로드가 선택사항입니다" + : "템플릿 파일을 업로드하세요"} + ( - 계약서 파일 * + 템플릿 파일 + {form.watch("templateName") !== "General GTC" && ( + * + )} + {form.watch("templateName") === "General GTC" && ( + (선택사항) + )} {selectedFile ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` - : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} + : form.watch("templateName") === "General GTC" + ? "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (선택사항, 최대 100MB)" + : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} @@ -488,7 +491,7 @@ export function AddTemplateDialog() { )} /> - + {showProgress && (

@@ -504,7 +507,6 @@ export function AddTemplateDialog() {
- {/* 고정된 푸터 */} @@ -525,4 +527,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 b7c2fa08..5783ca27 100644 --- a/lib/basic-contract/template/basic-contract-template-columns.tsx +++ b/lib/basic-contract/template/basic-contract-template-columns.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" -import { Download, Ellipsis, Paperclip, CheckCircle, XCircle, Eye } from "lucide-react" +import { Download, Ellipsis, Paperclip, CheckCircle, XCircle, Eye, Copy, GitBranch } from "lucide-react" import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" @@ -119,7 +119,13 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef const template = row.original; const handleViewDetails = () => { - router.push(`/evcp/basic-contract-template/${template.id}`); + // templateName이 "General GTC"인 경우 특별한 라우팅 + if (template.templateName === "General GTC") { + router.push(`/evcp/basic-contract-template/gtc`); + } else { + // 일반적인 경우는 기존과 동일 + router.push(`/evcp/basic-contract-template/${template.id}`); + } }; return ( @@ -133,25 +139,34 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef