summaryrefslogtreecommitdiff
path: root/lib/basic-contract/template/add-basic-contract-template-dialog.tsx
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/add-basic-contract-template-dialog.tsx
parentd78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff)
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/basic-contract/template/add-basic-contract-template-dialog.tsx')
-rw-r--r--lib/basic-contract/template/add-basic-contract-template-dialog.tsx364
1 files changed, 185 insertions, 179 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