summaryrefslogtreecommitdiff
path: root/lib/vendor-investigation
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-investigation')
-rw-r--r--lib/vendor-investigation/table/investigation-result-sheet.tsx49
-rw-r--r--lib/vendor-investigation/validations.ts89
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