summaryrefslogtreecommitdiff
path: root/lib/basic-contract/template
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
commit02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch)
treee932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /lib/basic-contract/template
parentd78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff)
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/basic-contract/template')
-rw-r--r--lib/basic-contract/template/add-basic-contract-template-dialog.tsx364
-rw-r--r--lib/basic-contract/template/basic-contract-template-columns.tsx314
-rw-r--r--lib/basic-contract/template/create-revision-dialog.tsx21
-rw-r--r--lib/basic-contract/template/template-editor-wrapper.tsx106
-rw-r--r--lib/basic-contract/template/update-basicContract-sheet.tsx245
5 files changed, 459 insertions, 591 deletions
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<typeof templateFormSchema>;
@@ -120,21 +110,16 @@ export function AddTemplateDialog() {
const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
const [uploadProgress, setUploadProgress] = React.useState(0);
const [showProgress, setShowProgress] = React.useState(false);
- const [availableTemplateNames, setAvailableTemplateNames] = React.useState<typeof TEMPLATE_NAME_OPTIONS[number][]>(TEMPLATE_NAME_OPTIONS);
+ const [availableFixedTemplates, setAvailableFixedTemplates] = React.useState<typeof FIXED_TEMPLATE_OPTIONS[number][]>([]);
+ const [availableProjects, setAvailableProjects] = React.useState<ProjectForFilter[]>([]);
const router = useRouter();
// 기본값
const defaultValues: Partial<TemplateFormValues> = {
- 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<TemplateFormValues>({
@@ -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 (
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
@@ -332,13 +307,61 @@ export function AddTemplateDialog() {
<div className="flex-1 overflow-y-auto px-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-4">
+ {/* 템플릿 타입 선택 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">템플릿 종류</CardTitle>
+ <CardDescription>
+ 추가할 템플릿의 종류를 선택하세요
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <FormField
+ control={form.control}
+ name="templateType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ 템플릿 종류 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select
+ onValueChange={(value) => {
+ field.onChange(value);
+ // 타입 변경 시 관련 필드 초기화
+ form.setValue("templateName", "");
+ form.setValue("selectedProjectId", undefined);
+ }}
+ value={field.value}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="템플릿 종류를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="FIXED">표준 템플릿</SelectItem>
+ <SelectItem value="PROJECT_GTC">프로젝트 GTC</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormDescription>
+ {templateType === 'FIXED' && "미리 정의된 표준 템플릿 중에서 선택합니다."}
+ {templateType === 'PROJECT_GTC' && "특정 프로젝트용 GTC 템플릿을 생성합니다."}
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ </Card>
+
{/* 기본 정보 */}
<Card>
<CardHeader>
<CardTitle className="text-lg">기본 정보</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
- <div className="grid grid-cols-1 gap-4">
+ {/* 표준 템플릿 선택 */}
+ {templateType === 'FIXED' && (
<FormField
control={form.control}
name="templateName"
@@ -349,16 +372,16 @@ export function AddTemplateDialog() {
</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
- <SelectTrigger disabled={availableTemplateNames.length === 0}>
+ <SelectTrigger disabled={availableFixedTemplates.length === 0}>
<SelectValue placeholder={
- availableTemplateNames.length === 0
+ availableFixedTemplates.length === 0
? "사용 가능한 템플릿이 없습니다"
: "템플릿 이름을 선택하세요"
} />
</SelectTrigger>
</FormControl>
<SelectContent>
- {availableTemplateNames.map((option) => (
+ {availableFixedTemplates.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
@@ -372,7 +395,57 @@ export function AddTemplateDialog() {
</FormItem>
)}
/>
- </div>
+ )}
+
+ {/* 프로젝트 GTC */}
+ {templateType === 'PROJECT_GTC' && (
+ <>
+ <FormField
+ control={form.control}
+ name="selectedProjectId"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ 프로젝트 선택 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select
+ onValueChange={handleProjectChange}
+ value={field.value?.toString()}
+ >
+ <FormControl>
+ <SelectTrigger disabled={availableProjects.length === 0}>
+ <SelectValue placeholder={
+ availableProjects.length === 0
+ ? "GTC 생성 가능한 프로젝트가 없습니다"
+ : "프로젝트를 선택하세요"
+ } />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {availableProjects.map((project) => (
+ <SelectItem key={project.id} value={project.id.toString()}>
+ {project.code} - {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormDescription>
+ 아직 GTC가 생성되지 않은 프로젝트만 표시됩니다.
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 생성될 템플릿 이름 미리보기 */}
+ {templateName && (
+ <div className="rounded-lg border p-3 bg-muted/50">
+ <div className="text-sm font-medium">생성될 템플릿 이름</div>
+ <div className="text-lg font-semibold text-primary">{templateName}</div>
+ </div>
+ )}
+ </>
+ )}
<FormField
control={form.control}
@@ -397,71 +470,12 @@ export function AddTemplateDialog() {
</CardContent>
</Card>
- {/* 적용 범위 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">
- 적용 범위 <span className="text-red-500">*</span>
- </CardTitle>
- <CardDescription>
- 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨)
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="flex items-center space-x-2">
- <Checkbox
- id="select-all"
- checked={selectedScopesCount === BUSINESS_UNITS.length}
- onCheckedChange={handleSelectAllScopes}
- />
- <label htmlFor="select-all" className="text-sm font-medium">
- 전체 선택
- </label>
- </div>
-
- <Separator />
-
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- {BUSINESS_UNITS.map((unit) => (
- <FormField
- key={unit.key}
- control={form.control}
- name={unit.key as keyof TemplateFormValues}
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value as boolean}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none">
- <FormLabel className="text-sm font-normal">
- {unit.label}
- </FormLabel>
- </div>
- </FormItem>
- )}
- />
- ))}
- </div>
-
- {form.formState.errors.shipBuildingApplicable && (
- <p className="text-sm text-destructive">
- {form.formState.errors.shipBuildingApplicable.message}
- </p>
- )}
- </CardContent>
- </Card>
-
{/* 파일 업로드 */}
<Card>
<CardHeader>
<CardTitle className="text-lg">파일 업로드</CardTitle>
<CardDescription>
- {form.watch("templateName") === "General GTC"
- ? "General GTC는 파일 업로드가 선택사항입니다"
- : "템플릿 파일을 업로드하세요"}
+ 템플릿 파일을 업로드하세요
</CardDescription>
</CardHeader>
<CardContent>
@@ -471,13 +485,7 @@ export function AddTemplateDialog() {
render={() => (
<FormItem>
<FormLabel>
- 템플릿 파일
- {form.watch("templateName") !== "General GTC" && (
- <span className="text-red-500"> *</span>
- )}
- {form.watch("templateName") === "General GTC" && (
- <span className="text-muted-foreground"> (선택사항)</span>
- )}
+ 템플릿 파일 <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Dropzone
@@ -495,9 +503,7 @@ export function AddTemplateDialog() {
<DropzoneDescription>
{selectedFile
? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`
- : form.watch("templateName") === "General GTC"
- ? "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (선택사항, 최대 100MB)"
- : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"}
+ : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"}
</DropzoneDescription>
<DropzoneInput />
</DropzoneZone>
@@ -543,4 +549,4 @@ export function AddTemplateDialog() {
</DialogContent>
</Dialog>
);
-}
+} \ 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<BasicContractTemplate>[] = [
- {
- accessorKey: "shipBuildingApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="조선해양" />,
- cell: ({ row }) => {
- const applicable = row.getValue("shipBuildingApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 80,
- enableResizing: true,
- },
- {
- accessorKey: "windApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="풍력" />,
- cell: ({ row }) => {
- const applicable = row.getValue("windApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 60,
- enableResizing: true,
- },
- {
- accessorKey: "pcApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PC" />,
- cell: ({ row }) => {
- const applicable = row.getValue("pcApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 50,
- enableResizing: true,
- },
- {
- accessorKey: "nbApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="NB" />,
- cell: ({ row }) => {
- const applicable = row.getValue("nbApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 50,
- enableResizing: true,
- },
- {
- accessorKey: "rcApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="RC" />,
- cell: ({ row }) => {
- const applicable = row.getValue("rcApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 50,
- enableResizing: true,
- },
- {
- accessorKey: "gyApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="GY" />,
- cell: ({ row }) => {
- const applicable = row.getValue("gyApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 50,
- enableResizing: true,
- },
- {
- accessorKey: "sysApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="S&Sys" />,
- cell: ({ row }) => {
- const applicable = row.getValue("sysApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 60,
- enableResizing: true,
- },
- {
- accessorKey: "infraApplicable",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Infra" />,
- cell: ({ row }) => {
- const applicable = row.getValue("infraApplicable") as boolean;
- return (
- <div className="flex justify-center">
- {applicable ? (
- <CheckCircle className="h-4 w-4 text-green-500" />
- ) : (
- <XCircle className="h-4 w-4 text-gray-300" />
- )}
- </div>
- );
- },
- size: 60,
- enableResizing: true,
- },
- ];
+ // const scopeColumns: ColumnDef<BasicContractTemplate>[] = [
+ // {
+ // accessorKey: "shipBuildingApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="조선해양" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("shipBuildingApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 80,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "windApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="풍력" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("windApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 60,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "pcApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PC" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("pcApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "nbApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="NB" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("nbApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "rcApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="RC" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("rcApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "gyApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="GY" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("gyApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "sysApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="S&Sys" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("sysApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 60,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "infraApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Infra" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("infraApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 60,
+ // enableResizing: true,
+ // },
+ // ];
// 파일 정보 그룹
const fileInfoColumns: ColumnDef<BasicContractTemplate>[] = [
@@ -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<typeof createRevisionSchema>;
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<void>;
}
-// 템플릿 이름별 변수 매핑 (영문 변수명 사용)
+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<string>("");
const [predefinedVariables, setPredefinedVariables] = React.useState<string[]>([]);
- 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({
<p className="text-xs text-muted-foreground mb-2">
{templateName ? `${templateName}에 권장되는 변수` : "자주 사용하는 변수"} (클릭하여 복사):
</p>
- <div className="flex flex-wrap gap-1">
- {predefinedVariables.map((variable, index) => (
- <Button
- key={index}
- variant="ghost"
- size="sm"
- className="h-6 px-2 text-xs hover:bg-blue-50"
- onClick={() => insertVariable(variable)}
- >
- {`{{${variable}}}`}
- </Button>
- ))}
- </div>
+ <TooltipProvider>
+ <div className="flex flex-wrap gap-1">
+ {predefinedVariables.map((variable, index) => (
+ <Tooltip key={index}>
+ <TooltipTrigger asChild>
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-6 px-2 text-xs hover:bg-blue-50"
+ onClick={() => insertVariable(variable)}
+ >
+ {`{{${variable}}}`}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>{VARIABLE_DESCRIPTION_MAP[variable as keyof typeof VARIABLE_DESCRIPTION_MAP] || variable}</p>
+ </TooltipContent>
+ </Tooltip>
+ ))}
+ </div>
+ </TooltipProvider>
</div>
)}
</div>
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,
@@ -20,14 +19,6 @@ import {
FormDescription,
} from "@/components/ui/form"
import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
Sheet,
SheetClose,
SheetContent,
@@ -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<typeof updateTemplateSchema>
@@ -122,16 +73,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
const form = useForm<UpdateTemplateSchema>({
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 (
<Sheet {...props}>
- <SheetContent className="sm:max-w-[600px] h-[100vh] flex flex-col p-0">
+ <SheetContent className="sm:max-w-[500px] h-[100vh] flex flex-col p-0">
{/* 고정된 헤더 */}
<SheetHeader className="p-6 pb-4 border-b">
<SheetTitle>템플릿 업데이트</SheetTitle>
<SheetDescription>
- 템플릿 정보를 수정하고 변경사항을 저장하세요
- <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
+ 템플릿 파일을 업데이트하고 설정을 변경하세요
</SheetDescription>
</SheetHeader>
@@ -249,51 +152,49 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 py-4"
>
- {/* 기본 정보 */}
+ {/* 템플릿 정보 표시 */}
<Card>
<CardHeader>
- <CardTitle className="text-lg">기본 정보</CardTitle>
+ <CardTitle className="text-lg">템플릿 정보</CardTitle>
<CardDescription>
- 현재 리비전: <Badge variant="outline">v{template.revision}</Badge>
- <br />
- 현재 적용 범위: {scopeHelpers.getScopeDisplayText(template)}
+ 현재 템플릿의 기본 정보입니다
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4">
- <FormField
- control={form.control}
- name="templateName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>
- 템플릿 이름 <span className="text-red-500">*</span>
- </FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="템플릿 이름을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectGroup>
- {TEMPLATE_NAME_OPTIONS.map((option) => (
- <SelectItem key={option} value={option}>
- {option}
- </SelectItem>
- ))}
- </SelectGroup>
- </SelectContent>
- </Select>
- <FormDescription>
- 미리 정의된 템플릿 중에서 선택
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
+ <div className="space-y-2">
+ <label className="text-sm font-medium">템플릿 이름</label>
+ <div className="px-3 py-2 border rounded-md bg-gray-50">
+ {template.templateName}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">현재 리비전</label>
+ <div className="px-3 py-2 border rounded-md bg-gray-50">
+ <Badge variant="outline">v{template.revision}</Badge>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">현재 파일</label>
+ <div className="px-3 py-2 border rounded-md bg-gray-50">
+ {template.fileName}
+ </div>
+ </div>
</div>
+ </CardContent>
+ </Card>
+ {/* 설정 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">설정</CardTitle>
+ <CardDescription>
+ 템플릿 관련 설정을 변경할 수 있습니다
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
<FormField
control={form.control}
name="legalReviewRequired"
@@ -317,69 +218,12 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
</CardContent>
</Card>
- {/* 적용 범위 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">
- 적용 범위 <span className="text-red-500">*</span>
- </CardTitle>
- <CardDescription>
- 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨)
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="flex items-center space-x-2">
- <Checkbox
- id="select-all"
- checked={selectedScopesCount === BUSINESS_UNITS.length}
- onCheckedChange={handleSelectAllScopes}
- />
- <label htmlFor="select-all" className="text-sm font-medium">
- 전체 선택
- </label>
- </div>
-
- <Separator />
-
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- {BUSINESS_UNITS.map((unit) => (
- <FormField
- key={unit.key}
- control={form.control}
- name={unit.key as keyof UpdateTemplateSchema}
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value as boolean}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none">
- <FormLabel className="text-sm font-normal">
- {unit.label}
- </FormLabel>
- </div>
- </FormItem>
- )}
- />
- ))}
- </div>
-
- {form.formState.errors.shipBuildingApplicable && (
- <p className="text-sm text-destructive">
- {form.formState.errors.shipBuildingApplicable.message}
- </p>
- )}
- </CardContent>
- </Card>
-
{/* 파일 업데이트 */}
<Card>
<CardHeader>
<CardTitle className="text-lg">파일 업데이트</CardTitle>
<CardDescription>
- 현재 파일: {template.fileName}
+ 새로운 템플릿 파일을 업로드하세요
</CardDescription>
</CardHeader>
<CardContent>
@@ -388,7 +232,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
name="file"
render={() => (
<FormItem>
- <FormLabel>템플릿 파일 (선택사항)</FormLabel>
+ <FormLabel>새 템플릿 파일 (선택사항)</FormLabel>
<FormControl>
<Dropzone
onDrop={handleFileChange}
@@ -402,7 +246,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
<DropzoneTitle>
{selectedFile
? selectedFile.name
- : "새 워드 파일을 드래그하세요 (선택사항)"}
+ : "새 워드 파일을 드래그하세요"}
</DropzoneTitle>
<DropzoneDescription>
{selectedFile
@@ -413,6 +257,9 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
</DropzoneZone>
</Dropzone>
</FormControl>
+ <FormDescription>
+ 파일을 업로드하지 않으면 기존 파일이 유지됩니다
+ </FormDescription>
<FormMessage />
</FormItem>
)}
@@ -433,12 +280,12 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
<Button
type="button"
onClick={form.handleSubmit(onSubmit)}
- disabled={isDisabled}
+ disabled={isUpdatePending}
>
{isUpdatePending && (
<Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
)}
- 저장
+ 업데이트
</Button>
</SheetFooter>
</SheetContent>