diff options
Diffstat (limited to 'lib/vendor-investigation')
| -rw-r--r-- | lib/vendor-investigation/table/investigation-result-sheet.tsx | 49 | ||||
| -rw-r--r-- | lib/vendor-investigation/validations.ts | 89 |
2 files changed, 118 insertions, 20 deletions
diff --git a/lib/vendor-investigation/table/investigation-result-sheet.tsx b/lib/vendor-investigation/table/investigation-result-sheet.tsx index 36000333..740b837e 100644 --- a/lib/vendor-investigation/table/investigation-result-sheet.tsx +++ b/lib/vendor-investigation/table/investigation-result-sheet.tsx @@ -129,10 +129,11 @@ export function InvestigationResultSheet({ defaultValues: { investigationId: investigation?.investigationId ?? 0, completedAt: investigation?.completedAt ?? undefined, + requestedAt: investigation?.requestedAt ?? undefined, // 날짜 검증을 위해 추가 evaluationScore: investigation?.evaluationScore ?? undefined, evaluationResult: investigation?.evaluationResult ?? undefined, investigationNotes: investigation?.investigationNotes ?? "", - attachments: undefined, + attachments: [], }, }) @@ -175,10 +176,11 @@ export function InvestigationResultSheet({ form.reset({ investigationId: investigation.investigationId, completedAt: investigation.completedAt ?? undefined, + requestedAt: investigation.requestedAt ?? undefined, // 날짜 검증을 위해 추가 evaluationScore: investigation.evaluationScore ?? undefined, evaluationResult: investigation.evaluationResult ?? undefined, investigationNotes: investigation.investigationNotes ?? "", - attachments: undefined, + attachments: [], }) // 기존 첨부파일 로드 @@ -240,9 +242,9 @@ export function InvestigationResultSheet({ // 선택된 파일에서 특정 파일 제거 const handleRemoveSelectedFile = (indexToRemove: number) => { - const currentFiles = form.getValues("attachments") || [] + const currentFiles = (form.getValues("attachments") as File[]) || [] const updatedFiles = currentFiles.filter((_: File, index: number) => index !== indexToRemove) - form.setValue("attachments", updatedFiles.length > 0 ? updatedFiles : undefined) + form.setValue("attachments", updatedFiles as any) if (updatedFiles.length === 0) { toast.success("모든 선택된 파일이 제거되었습니다.") @@ -326,7 +328,6 @@ export function InvestigationResultSheet({ name="attachments" render={({ field: { onChange, ...field } }) => ( <FormItem> - <FormLabel>{config.label}</FormLabel> <FormControl> <Dropzone onDrop={(acceptedFiles, rejectedFiles) => { @@ -346,7 +347,7 @@ export function InvestigationResultSheet({ if (acceptedFiles.length > 0) { // 기존 파일들과 새로 선택된 파일들을 합치기 - const currentFiles = form.getValues("attachments") || [] + const currentFiles = (form.getValues("attachments") as File[]) || [] const newFiles = [...currentFiles, ...acceptedFiles] onChange(newFiles) toast.success(`${acceptedFiles.length}개 파일이 추가되었습니다.`) @@ -513,13 +514,23 @@ export function InvestigationResultSheet({ } } - // 2) 파일이 있으면 업로드 - if (values.attachments && values.attachments.length > 0) { + // 2) 첨부파일 검증 (필수: 기존 첨부파일 + 새 파일 합계 최소 1개) + const newFilesCount = values.attachments?.length || 0 + const existingFilesCount = existingAttachments.length + const totalFilesCount = newFilesCount + existingFilesCount + + if (totalFilesCount === 0) { + toast.error("최소 1개의 첨부파일이 필요합니다.") + return + } + + // 새 파일이 있는 경우에만 업로드 진행 + if (newFilesCount > 0) { setUploadingFiles(true) try { await uploadFiles(values.attachments, values.investigationId) - toast.success(`실사 결과와 ${values.attachments.length}개 파일이 업데이트되었습니다!`) + toast.success(`실사 결과와 ${newFilesCount}개 파일이 업데이트되었습니다!`) // 첨부파일 목록 새로고침 loadExistingAttachments(values.investigationId) @@ -530,6 +541,7 @@ export function InvestigationResultSheet({ setUploadingFiles(false) } } else { + // 기존 첨부파일만 있는 경우 toast.success("실사 결과가 업데이트되었습니다!") } @@ -668,7 +680,14 @@ export function InvestigationResultSheet({ if (result === "APPROVED") return <span className="text-green-600">합격 (승인)</span> if (result === "SUPPLEMENT") return <span className="text-yellow-600">보완 필요 (방법 선택)</span> if (result === "SUPPLEMENT_REINSPECT") return <span className="text-yellow-600">보완 필요 - 재실사</span> - if (result === "SUPPLEMENT_DOCUMENT") return <span className="text-yellow-600">보완 필요 - 자료제출</span> + if (result === "SUPPLEMENT_DOCUMENT") return ( + <div className="flex flex-col gap-1"> + <span className="text-yellow-600">보완 필요 - 자료제출</span> + <span className="text-xs text-muted-foreground font-normal"> + 💡 저장 시 협력업체에 자동으로 보완 요청 메일이 발송됩니다. + </span> + </div> + ) if (result === "REJECTED") return <span className="text-destructive">불합격</span> return <span className="text-muted-foreground">-</span> })()} @@ -711,8 +730,14 @@ export function InvestigationResultSheet({ )} /> - {/* 파일 첨부 섹션 */} - {renderFileUploadSection()} + {/* 파일 첨부 섹션 */} + <div className="space-y-2"> + <FormLabel>실사 관련 첨부파일<span className="text-red-500 ml-1">*</span></FormLabel> + <div className="text-sm text-muted-foreground"> + 실사 결과 입력 시 최소 1개의 첨부파일이 필요합니다. + </div> + {renderFileUploadSection()} + </div> </form> </Form> </div> diff --git a/lib/vendor-investigation/validations.ts b/lib/vendor-investigation/validations.ts index 891ef178..84361ef9 100644 --- a/lib/vendor-investigation/validations.ts +++ b/lib/vendor-investigation/validations.ts @@ -98,6 +98,20 @@ export const updateVendorInvestigationProgressSchema = z message: "실사 수행 예정일은 필수입니다.", }) } + + // 날짜 순서 검증: forecastedAt과 confirmedAt 간의 관계 검증 + if (data.forecastedAt && data.confirmedAt) { + const forecastedDate = new Date(data.forecastedAt); + const confirmedDate = new Date(data.confirmedAt); + + if (confirmedDate < forecastedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["confirmedAt"], + message: "실사 계획 확정일은 실사 수행 예정일보다 과거일 수 없습니다.", + }); + } + } }) export type UpdateVendorInvestigationProgressSchema = z.infer<typeof updateVendorInvestigationProgressSchema> @@ -114,15 +128,33 @@ export const updateVendorInvestigationResultSchema = z.object({ z.string().transform((str) => str ? new Date(str) : undefined) ]), + // 실사의뢰일 (날짜 검증을 위해 추가) + requestedAt: z.union([ + z.date(), + z.string().transform((str) => str ? new Date(str) : undefined) + ]).optional(), + evaluationScore: z.number() .int("평가 점수는 정수여야 합니다.") .min(0, "평가 점수는 0점 이상이어야 합니다.") .max(100, "평가 점수는 100점 이하여야 합니다."), evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "SUPPLEMENT_REINSPECT", "SUPPLEMENT_DOCUMENT", "REJECTED", "RESULT_SENT"]), investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(), - attachments: z.any({ - required_error: "첨부파일은 필수입니다." - }), + attachments: z.array(z.any()).min(1, "최소 1개의 첨부파일이 필요합니다."), +}).superRefine((data, ctx) => { + // 날짜 검증: 실제 실사일이 실사의뢰일보다 과거가 되지 않도록 검증 + if (data.requestedAt && data.completedAt) { + const requestedDate = new Date(data.requestedAt); + const completedDate = new Date(data.completedAt); + + if (completedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["completedAt"], + message: "실제 실사일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } }) export type UpdateVendorInvestigationResultSchema = z.infer<typeof updateVendorInvestigationResultSchema> @@ -137,28 +169,28 @@ export const updateVendorInvestigationSchema = z.object({ }), investigationAddress: z.string().optional(), investigationMethod: z.enum(["PURCHASE_SELF_EVAL", "DOCUMENT_EVAL", "PRODUCT_INSPECTION", "SITE_VISIT_EVAL"]).optional(), - + // 날짜 필드들 forecastedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + requestedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + confirmedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + completedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + evaluationScore: z.number() .int("평가 점수는 정수여야 합니다.") .min(0, "평가 점수는 0점 이상이어야 합니다.") @@ -167,6 +199,47 @@ export const updateVendorInvestigationSchema = z.object({ evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "SUPPLEMENT_REINSPECT", "SUPPLEMENT_DOCUMENT", "REJECTED", "RESULT_SENT"]).optional(), investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(), attachments: z.any().optional(), // File 업로드를 위한 필드 +}).superRefine((data, ctx) => { + // 날짜 검증: 실사의뢰일(requestedAt)이 있는 경우 다른 날짜들이 실사의뢰일보다 과거가 되지 않도록 검증 + if (data.requestedAt) { + const requestedDate = new Date(data.requestedAt); + + // 실사수행/계획일(forecastedAt) 검증 + if (data.forecastedAt) { + const forecastedDate = new Date(data.forecastedAt); + if (forecastedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["forecastedAt"], + message: "실사 수행 예정일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } + + // 실사 계획 확정일(confirmedAt) 검증 + if (data.confirmedAt) { + const confirmedDate = new Date(data.confirmedAt); + if (confirmedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["confirmedAt"], + message: "실사 계획 확정일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } + + // 실제 실사일(completedAt) 검증 + if (data.completedAt) { + const completedDate = new Date(data.completedAt); + if (completedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["completedAt"], + message: "실제 실사일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } + } }) export type UpdateVendorInvestigationSchema = z.infer<typeof updateVendorInvestigationSchema>
\ No newline at end of file |
