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 --- .../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 +++----------- 5 files changed, 459 insertions(+), 591 deletions(-) (limited to 'lib/basic-contract/template') 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
-- cgit v1.2.3