summaryrefslogtreecommitdiff
path: root/lib/vendor-investigation/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-investigation/table')
-rw-r--r--lib/vendor-investigation/table/investigation-progress-sheet.tsx324
-rw-r--r--lib/vendor-investigation/table/investigation-result-sheet.tsx808
-rw-r--r--lib/vendor-investigation/table/investigation-table-columns.tsx110
-rw-r--r--lib/vendor-investigation/table/investigation-table.tsx51
-rw-r--r--lib/vendor-investigation/table/update-investigation-sheet.tsx144
5 files changed, 1340 insertions, 97 deletions
diff --git a/lib/vendor-investigation/table/investigation-progress-sheet.tsx b/lib/vendor-investigation/table/investigation-progress-sheet.tsx
new file mode 100644
index 00000000..c0357f5c
--- /dev/null
+++ b/lib/vendor-investigation/table/investigation-progress-sheet.tsx
@@ -0,0 +1,324 @@
+"use client"
+
+import * as React from "react"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { CalendarIcon, Loader } from "lucide-react"
+import { format } from "date-fns"
+import { toast } from "sonner"
+
+import { Button } from "@/components/ui/button"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Sheet,
+ SheetClose,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Calendar } from "@/components/ui/calendar"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+
+import {
+ updateVendorInvestigationProgressSchema,
+ type UpdateVendorInvestigationProgressSchema,
+} from "../validations"
+import { updateVendorInvestigationProgressAction } from "../service"
+import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig"
+
+interface InvestigationProgressSheetProps
+ extends React.ComponentPropsWithoutRef<typeof Sheet> {
+ investigation: VendorInvestigationsViewWithContacts | null
+}
+
+/**
+ * 실사 진행 관리 시트
+ */
+export function InvestigationProgressSheet({
+ investigation,
+ ...props
+}: InvestigationProgressSheetProps) {
+ const [isPending, startTransition] = React.useTransition()
+
+ // RHF + Zod
+ const form = useForm<UpdateVendorInvestigationProgressSchema>({
+ resolver: zodResolver(updateVendorInvestigationProgressSchema),
+ defaultValues: {
+ investigationId: investigation?.investigationId ?? 0,
+ investigationAddress: investigation?.investigationAddress ?? "",
+ investigationMethod: investigation?.investigationMethod ?? undefined,
+ forecastedAt: investigation?.forecastedAt ?? undefined,
+ confirmedAt: investigation?.confirmedAt ?? undefined,
+ },
+ })
+
+ // investigation이 변경될 때마다 폼 리셋
+ React.useEffect(() => {
+ if (investigation) {
+ form.reset({
+ investigationId: investigation.investigationId,
+ investigationAddress: investigation.investigationAddress ?? "",
+ investigationMethod: investigation.investigationMethod ?? undefined,
+ forecastedAt: investigation.forecastedAt ?? undefined,
+ confirmedAt: investigation.confirmedAt ?? undefined,
+ })
+ }
+ }, [investigation, form])
+
+ // Submit handler
+ async function onSubmit(values: UpdateVendorInvestigationProgressSchema) {
+ console.log("실사 진행 관리 onSubmit 호출됨:", values)
+
+ if (!values.investigationId) {
+ console.log("investigationId가 없음:", values.investigationId)
+ return
+ }
+
+ startTransition(async () => {
+ try {
+ console.log("실사 진행 관리 startTransition 시작")
+
+ // FormData 생성
+ const formData = new FormData()
+
+ // 필수 필드
+ formData.append("investigationId", String(values.investigationId))
+
+ // 선택적 필드들
+ if (values.investigationAddress) {
+ formData.append("investigationAddress", values.investigationAddress)
+ }
+
+ if (values.investigationMethod) {
+ formData.append("investigationMethod", values.investigationMethod)
+ }
+
+ if (values.forecastedAt) {
+ formData.append("forecastedAt", values.forecastedAt.toISOString())
+ }
+
+ if (values.confirmedAt) {
+ formData.append("confirmedAt", values.confirmedAt.toISOString())
+ }
+
+ // 실사 진행 관리 업데이트 (PLANNED -> IN_PROGRESS)
+ const { error } = await updateVendorInvestigationProgressAction(formData)
+
+ if (error) {
+ toast.error(error)
+ return
+ }
+
+ toast.success("실사 진행 정보가 업데이트되었습니다!")
+ form.reset()
+ props.onOpenChange?.(false)
+
+ } catch (error) {
+ console.error("실사 진행 관리 업데이트 오류:", error)
+ toast.error("실사 진행 관리 업데이트 중 오류가 발생했습니다.")
+ }
+ })
+ }
+
+ // 디버깅을 위한 버튼 클릭 핸들러
+ const handleSaveClick = async () => {
+ console.log("실사 진행 관리 저장 버튼 클릭됨")
+ console.log("현재 폼 값:", form.getValues())
+ console.log("폼 에러:", form.formState.errors)
+
+ // 폼 검증 실행
+ const isValid = await form.trigger()
+ console.log("폼 검증 결과:", isValid)
+
+ if (isValid) {
+ form.handleSubmit(onSubmit)()
+ } else {
+ console.log("폼 검증 실패, 에러:", form.formState.errors)
+ }
+ }
+
+ return (
+ <Sheet {...props}>
+ <SheetContent className="flex flex-col h-full sm:max-w-xl" >
+ <SheetHeader className="text-left flex-shrink-0">
+ <SheetTitle>실사 진행 관리</SheetTitle>
+ <SheetDescription>
+ {investigation?.vendorName && (
+ <span className="font-medium">{investigation.vendorName}</span>
+ )}의 실사 진행 정보를 관리합니다.
+ </SheetDescription>
+ </SheetHeader>
+
+ <div className="flex-1 overflow-y-auto py-4">
+ <Form {...form}>
+ <form
+ onSubmit={form.handleSubmit(onSubmit)}
+ className="flex flex-col gap-4"
+ id="investigation-progress-form"
+ >
+ {/* 실사 주소 */}
+ <FormField
+ control={form.control}
+ name="investigationAddress"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>실사 주소</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="실사가 진행될 주소를 입력하세요..."
+ {...field}
+ className="min-h-[60px]"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 실사 방법 */}
+ <FormField
+ control={form.control}
+ name="investigationMethod"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>실사 방법</FormLabel>
+ <FormControl>
+ <Select value={field.value || ""} onValueChange={field.onChange}>
+ <SelectTrigger>
+ <SelectValue placeholder="실사 방법을 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ <SelectItem value="PURCHASE_SELF_EVAL">구매자체평가</SelectItem>
+ <SelectItem value="DOCUMENT_EVAL">서류평가</SelectItem>
+ <SelectItem value="PRODUCT_INSPECTION">제품검사평가</SelectItem>
+ <SelectItem value="SITE_VISIT_EVAL">방문실사평가</SelectItem>
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 실사 수행 예정일 */}
+ <FormField
+ control={form.control}
+ name="forecastedAt"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>실사 수행 예정일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일")
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 실사 확정일 */}
+ <FormField
+ control={form.control}
+ name="confirmedAt"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>실사 계획 확정일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일")
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </form>
+ </Form>
+ </div>
+
+ {/* Footer Buttons */}
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0 flex-shrink-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline" disabled={isPending}>
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ disabled={isPending}
+ onClick={handleSaveClick}
+ >
+ {isPending && (
+ <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
+ )}
+ {isPending ? "저장 중..." : "저장"}
+ </Button>
+ </SheetFooter>
+ </SheetContent>
+ </Sheet>
+ )
+}
diff --git a/lib/vendor-investigation/table/investigation-result-sheet.tsx b/lib/vendor-investigation/table/investigation-result-sheet.tsx
new file mode 100644
index 00000000..b7577daa
--- /dev/null
+++ b/lib/vendor-investigation/table/investigation-result-sheet.tsx
@@ -0,0 +1,808 @@
+"use client"
+
+import * as React from "react"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { CalendarIcon, Loader, X, Download } from "lucide-react"
+import { format } from "date-fns"
+import { toast } from "sonner"
+import { updateVendorInvestigationResultAction } from "../service"
+import { Button } from "@/components/ui/button"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Sheet,
+ SheetClose,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Calendar } from "@/components/ui/calendar"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+import {
+ Dropzone,
+ DropzoneZone,
+ DropzoneUploadIcon,
+ DropzoneTitle,
+ DropzoneDescription,
+ DropzoneInput
+} from "@/components/ui/dropzone"
+import {
+ FileList,
+ FileListAction,
+ FileListHeader,
+ FileListIcon,
+ FileListInfo,
+ FileListItem,
+ FileListName,
+ FileListSize,
+} from "@/components/ui/file-list"
+
+import {
+ updateVendorInvestigationResultSchema,
+ type UpdateVendorInvestigationResultSchema,
+} from "../validations"
+import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInvestigationAttachment, createVendorInvestigationAttachmentAction } from "../service"
+import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig"
+import prettyBytes from "pretty-bytes"
+import { downloadFile } from "@/lib/file-download"
+
+interface InvestigationResultSheetProps extends React.ComponentPropsWithoutRef<typeof Sheet> {
+ investigation: VendorInvestigationsViewWithContacts | null
+}
+
+// 첨부파일 정책 정의
+const getFileUploadConfig = (status: string) => {
+ // 취소된 상태에서만 파일 업로드 비활성화
+ if (status === "CANCELED") {
+ return {
+ enabled: false,
+ label: "",
+ description: "",
+ accept: undefined,
+ maxSize: 0,
+ maxSizeText: ""
+ }
+ }
+
+ // 모든 활성 상태에서 동일한 정책 적용
+ return {
+ enabled: true,
+ label: "실사 관련 첨부파일",
+ description: "실사와 관련된 모든 문서와 이미지를 첨부할 수 있습니다.",
+ accept: {
+ 'application/pdf': ['.pdf'],
+ 'application/msword': ['.doc'],
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
+ 'application/vnd.ms-excel': ['.xls'],
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
+ 'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
+ },
+ maxSize: 10 * 1024 * 1024, // 10MB
+ maxSizeText: "10MB"
+ }
+}
+
+/**
+ * 실사 결과 입력 시트
+ */
+export function InvestigationResultSheet({
+ investigation,
+ ...props
+}: InvestigationResultSheetProps) {
+ const [isPending, startTransition] = React.useTransition()
+ const [existingAttachments, setExistingAttachments] = React.useState<any[]>([])
+ const [loadingAttachments, setLoadingAttachments] = React.useState(false)
+ const [uploadingFiles, setUploadingFiles] = React.useState(false)
+
+ // RHF + Zod
+ const form = useForm<UpdateVendorInvestigationResultSchema>({
+ resolver: zodResolver(updateVendorInvestigationResultSchema),
+ defaultValues: {
+ investigationId: investigation?.investigationId ?? 0,
+ completedAt: investigation?.completedAt ?? undefined,
+ evaluationScore: investigation?.evaluationScore ?? undefined,
+ evaluationResult: investigation?.evaluationResult ?? undefined,
+ investigationNotes: investigation?.investigationNotes ?? "",
+ attachments: undefined,
+ },
+ })
+
+ // investigation이 변경될 때마다 폼 리셋
+ React.useEffect(() => {
+ if (investigation) {
+ form.reset({
+ investigationId: investigation.investigationId,
+ completedAt: investigation.completedAt ?? undefined,
+ evaluationScore: investigation.evaluationScore ?? undefined,
+ evaluationResult: investigation.evaluationResult ?? undefined,
+ investigationNotes: investigation.investigationNotes ?? "",
+ attachments: undefined,
+ })
+
+ // 기존 첨부파일 로드
+ loadExistingAttachments(investigation.investigationId)
+ }
+ }, [investigation, form])
+
+ // 기존 첨부파일 로드 함수
+ const loadExistingAttachments = async (investigationId: number) => {
+ setLoadingAttachments(true)
+ try {
+ const result = await getInvestigationAttachments(investigationId)
+ if (result.success) {
+ setExistingAttachments(result.attachments || [])
+ } else {
+ toast.error("첨부파일 목록을 불러오는데 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("첨부파일 로드 실패:", error)
+ toast.error("첨부파일 목록을 불러오는 중 오류가 발생했습니다.")
+ } finally {
+ setLoadingAttachments(false)
+ }
+ }
+
+ // 첨부파일 삭제 함수
+ const handleDeleteAttachment = async (attachmentId: number) => {
+ if (!investigation) return
+
+ try {
+ await deleteInvestigationAttachment(attachmentId)
+ toast.success("첨부파일이 삭제되었습니다.")
+ // 목록 새로고침
+ loadExistingAttachments(investigation.investigationId)
+
+ } catch (error) {
+ console.error("첨부파일 삭제 오류:", error)
+ toast.error(error instanceof Error ? error.message : "첨부파일 삭제 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 첨부파일 다운로드 함수
+ const handleDownloadAttachment = async (attachment: any) => {
+ if (!attachment.filePath || !attachment.fileName) {
+ toast.error("첨부파일 정보가 올바르지 않습니다.")
+ return
+ }
+
+ try {
+ await downloadFile(attachment.filePath, attachment.fileName, {
+ showToast: true,
+ action: 'download'
+ })
+ } catch (error) {
+ console.error("첨부파일 다운로드 오류:", error)
+ toast.error("첨부파일 다운로드 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 선택된 파일에서 특정 파일 제거
+ const handleRemoveSelectedFile = (indexToRemove: number) => {
+ const currentFiles = form.getValues("attachments") || []
+ const updatedFiles = currentFiles.filter((_: File, index: number) => index !== indexToRemove)
+ form.setValue("attachments", updatedFiles.length > 0 ? updatedFiles : undefined)
+
+ if (updatedFiles.length === 0) {
+ toast.success("모든 선택된 파일이 제거되었습니다.")
+ } else {
+ toast.success("파일이 제거되었습니다.")
+ }
+ }
+
+ // 파일 업로드 섹션 렌더링
+ const renderFileUploadSection = () => {
+ const currentStatus = form.watch("investigationStatus")
+ const selectedFiles = form.watch("attachments") as File[] | undefined
+ const config = getFileUploadConfig(currentStatus)
+
+ if (!config.enabled) return null
+
+ return (
+ <>
+ {/* 기존 첨부파일 목록 */}
+ {(existingAttachments.length > 0 || loadingAttachments) && (
+ <div className="space-y-2">
+ <FormLabel>기존 첨부파일</FormLabel>
+ <div className="border rounded-md p-3 space-y-2 max-h-32 overflow-y-auto">
+ {loadingAttachments ? (
+ <div className="flex items-center justify-center py-4">
+ <Loader className="h-4 w-4 animate-spin" />
+ <span className="ml-2 text-sm text-muted-foreground">
+ 첨부파일 로딩 중...
+ </span>
+ </div>
+ ) : existingAttachments.length > 0 ? (
+ existingAttachments.map((attachment) => (
+ <div key={attachment.id} className="flex items-center justify-between text-sm">
+ <div className="flex items-center space-x-2 flex-1 min-w-0">
+ <span className="text-xs px-2 py-1 bg-muted rounded">
+ {attachment.attachmentType}
+ </span>
+ <span className="truncate">{attachment.fileName}</span>
+ <span className="text-muted-foreground">
+ ({Math.round(attachment.fileSize / 1024)}KB)
+ </span>
+ </div>
+ <div className="flex items-center gap-1">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDownloadAttachment(attachment)}
+ className="text-blue-600 hover:text-blue-700"
+ disabled={isPending}
+ title="파일 다운로드"
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDeleteAttachment(attachment.id)}
+ className="text-destructive hover:text-destructive"
+ disabled={isPending}
+ title="파일 삭제"
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ </div>
+ ))
+ ) : (
+ <div className="text-sm text-muted-foreground text-center py-2">
+ 첨부된 파일이 없습니다.
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+
+ {/* 새 파일 업로드 */}
+ <FormField
+ control={form.control}
+ name="attachments"
+ render={({ field: { onChange, ...field } }) => (
+ <FormItem>
+ <FormLabel>{config.label}</FormLabel>
+ <FormControl>
+ <Dropzone
+ onDrop={(acceptedFiles, rejectedFiles) => {
+ // 거부된 파일에 대한 상세 에러 메시지
+ if (rejectedFiles.length > 0) {
+ rejectedFiles.forEach((file) => {
+ const error = file.errors[0]
+ if (error.code === 'file-too-large') {
+ toast.error(`${file.file.name}: 파일 크기가 ${config.maxSizeText}를 초과합니다.`)
+ } else if (error.code === 'file-invalid-type') {
+ toast.error(`${file.file.name}: 지원하지 않는 파일 형식입니다.`)
+ } else {
+ toast.error(`${file.file.name}: 파일 업로드에 실패했습니다.`)
+ }
+ })
+ }
+
+ if (acceptedFiles.length > 0) {
+ // 기존 파일들과 새로 선택된 파일들을 합치기
+ const currentFiles = form.getValues("attachments") || []
+ const newFiles = [...currentFiles, ...acceptedFiles]
+ onChange(newFiles)
+ toast.success(`${acceptedFiles.length}개 파일이 추가되었습니다.`)
+ }
+ }}
+ accept={config.accept}
+ multiple
+ maxSize={config.maxSize}
+ disabled={isPending || uploadingFiles}
+ >
+ <DropzoneZone>
+ <DropzoneUploadIcon />
+ <DropzoneTitle>
+ {isPending || uploadingFiles
+ ? "파일 업로드 중..."
+ : "파일을 드래그하거나 클릭하여 업로드"
+ }
+ </DropzoneTitle>
+ <DropzoneDescription>
+ {config.description} (최대 {config.maxSizeText})
+ </DropzoneDescription>
+ <DropzoneInput />
+ </DropzoneZone>
+ </Dropzone>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 선택된 파일 목록 */}
+ {selectedFiles && selectedFiles.length > 0 && (
+ <div className="space-y-2">
+ {/* <FormLabel>선택된 파일 ({selectedFiles.length}개)</FormLabel> */}
+ <FileList>
+ <FileListHeader>
+ <span className="text-sm font-medium">업로드 예정 파일 ({selectedFiles.length}개)</span>
+ </FileListHeader>
+ {selectedFiles.map((file, index) => (
+ <FileListItem
+ key={`${file.name}-${index}`}
+ className="flex items-center justify-between gap-2 px-2 py-2"
+ >
+ {/* 왼쪽 아이콘 */}
+ <FileListIcon className="shrink-0 h-4 w-4 text-muted-foreground" />
+
+ {/* 가운데 이름 + 사이즈 */}
+ <FileListInfo className="flex-1 min-w-0">
+ <FileListName className="truncate">{file.name}</FileListName>
+ <FileListSize className="text-xs text-muted-foreground shrink-0">
+ {file.size}
+ </FileListSize>
+ </FileListInfo>
+
+ {/* 오른쪽 삭제 버튼 */}
+ <FileListAction className="shrink-0">
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ onClick={() => handleRemoveSelectedFile(index)}
+ disabled={isPending || uploadingFiles}
+ className="h-5 w-5 text-destructive hover:text-destructive"
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </FileListAction>
+ </FileListItem>
+
+ ))}
+ </FileList>
+ </div>
+ )}
+ </>
+ )
+ }
+
+ // 파일 업로드 함수
+ const uploadFiles = async (files: File[], investigationId: number) => {
+ const uploadPromises = files.map(async (file) => {
+ try {
+ // 서버 액션을 호출하여 파일 저장 및 DB 레코드 생성
+ const result = await createVendorInvestigationAttachmentAction({
+ investigationId,
+ file,
+ userId: undefined // 필요시 사용자 ID 추가
+ });
+
+ if (!result.success) {
+ throw new Error(result.error || "파일 업로드 실패");
+ }
+
+ return result.attachment;
+ } catch (error) {
+ console.error(`파일 업로드 실패: ${file.name}`, error);
+ throw error;
+ }
+ });
+
+ return await Promise.all(uploadPromises);
+ }
+
+ // Submit handler
+ async function onSubmit(values: UpdateVendorInvestigationResultSchema) {
+ console.log("실사 결과 입력 onSubmit 호출됨:", values)
+
+ if (!values.investigationId) {
+ console.log("investigationId가 없음:", values.investigationId)
+ return
+ }
+
+ startTransition(async () => {
+ try {
+ console.log("실사 결과 입력 startTransition 시작")
+
+ // 1) 먼저 텍스트 데이터 업데이트
+ const formData = new FormData()
+
+ // 필수 필드
+ formData.append("investigationId", String(values.investigationId))
+
+ // 선택적 필드들
+ if (values.completedAt) {
+ formData.append("completedAt", values.completedAt.toISOString())
+ }
+
+ if (values.evaluationScore !== undefined) {
+ formData.append("evaluationScore", String(values.evaluationScore))
+ }
+
+ if (values.evaluationResult) {
+ formData.append("evaluationResult", values.evaluationResult)
+ }
+
+ if (values.investigationNotes) {
+ formData.append("investigationNotes", values.investigationNotes)
+ }
+
+ // 텍스트 데이터 업데이트 (IN_PROGRESS -> COMPLETED/CANCELED/SUPPLEMENT_REQUIRED)
+ const { error } = await updateVendorInvestigationResultAction(formData)
+
+ if (error) {
+ toast.error(error)
+ return
+ }
+
+ // 2) 파일이 있으면 업로드
+ if (values.attachments && values.attachments.length > 0) {
+ setUploadingFiles(true)
+
+ try {
+ await uploadFiles(values.attachments, values.investigationId)
+ toast.success(`실사 결과와 ${values.attachments.length}개 파일이 업데이트되었습니다!`)
+
+ // 첨부파일 목록 새로고침
+ loadExistingAttachments(values.investigationId)
+ } catch (fileError) {
+ console.error("파일 업로드 에러:", fileError)
+ toast.error(`데이터는 저장되었지만 파일 업로드 중 오류가 발생했습니다: ${fileError}`)
+ } finally {
+ setUploadingFiles(false)
+ }
+ } else {
+ toast.success("실사 결과가 업데이트되었습니다!")
+ }
+
+ form.reset()
+ props.onOpenChange?.(false)
+
+ } catch (error) {
+ console.error("실사 결과 업데이트 오류:", error)
+ toast.error("실사 결과 업데이트 중 오류가 발생했습니다.")
+ }
+ })
+ }
+
+ // 디버깅을 위한 버튼 클릭 핸들러
+ const handleSaveClick = async () => {
+ console.log("저장 버튼 클릭됨")
+ console.log("현재 폼 값:", form.getValues())
+ console.log("폼 에러:", form.formState.errors)
+
+ // 폼 검증 실행
+ const isValid = await form.trigger()
+ console.log("폼 검증 결과:", isValid)
+
+ if (isValid) {
+ form.handleSubmit(onSubmit)()
+ } else {
+ console.log("폼 검증 실패, 에러:", form.formState.errors)
+ }
+ }
+
+ return (
+ <Sheet {...props}>
+ <SheetContent className="flex flex-col h-full sm:max-w-xl" >
+ <SheetHeader className="text-left flex-shrink-0">
+ <SheetTitle>실사 결과 입력</SheetTitle>
+ <SheetDescription>
+ {investigation?.vendorName && (
+ <span className="font-medium">{investigation.vendorName}</span>
+ )}의 실사 결과를 입력합니다.
+ </SheetDescription>
+ </SheetHeader>
+
+ <div className="flex-1 overflow-y-auto py-4">
+ <Form {...form}>
+ <form
+ onSubmit={form.handleSubmit(onSubmit)}
+ className="flex flex-col gap-4"
+ id="update-investigation-form"
+ >
+ {/* 실사 상태 - 주석처리 (실사 결과 입력에서는 자동으로 완료됨/취소됨/보완요구됨으로 변경) */}
+ {/* <FormField
+ control={form.control}
+ name="investigationStatus"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>실사 상태</FormLabel>
+ <FormControl>
+ <Select value={field.value} onValueChange={field.onChange}>
+ <SelectTrigger>
+ <SelectValue placeholder="상태를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ <SelectItem value="PLANNED">계획됨</SelectItem>
+ <SelectItem value="IN_PROGRESS">진행 중</SelectItem>
+ <SelectItem value="COMPLETED">완료됨</SelectItem>
+ <SelectItem value="CANCELED">취소됨</SelectItem>
+ <SelectItem value="SUPPLEMENT_REQUIRED">보완 요구됨</SelectItem>
+ <SelectItem value="RESULT_SENT">실사결과발송</SelectItem>
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+
+ {/* 실사 주소 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
+ control={form.control}
+ name="investigationAddress"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>실사 주소</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="실사가 진행될 주소를 입력하세요..."
+ {...field}
+ className="min-h-[60px]"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+
+ {/* 실사 방법 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
+ control={form.control}
+ name="investigationMethod"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>실사 방법</FormLabel>
+ <FormControl>
+ <Select value={field.value || ""} onValueChange={field.onChange}>
+ <SelectTrigger>
+ <SelectValue placeholder="실사 방법을 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ <SelectItem value="PURCHASE_SELF_EVAL">구매자체평가</SelectItem>
+ <SelectItem value="DOCUMENT_EVAL">서류평가</SelectItem>
+ <SelectItem value="PRODUCT_INSPECTION">제품검사평가</SelectItem>
+ <SelectItem value="SITE_VISIT_EVAL">방문실사평가</SelectItem>
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+
+ {/* 실사 수행 예정일 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
+ control={form.control}
+ name="forecastedAt"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>실사 수행 예정일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일")
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+
+ {/* 실사 확정일 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
+ control={form.control}
+ name="confirmedAt"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>실사 계획 확정일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일")
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+
+ {/* 실제 실사일 */}
+ <FormField
+ control={form.control}
+ name="completedAt"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>실제 실사일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일")
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 평가 점수 */}
+ <FormField
+ control={form.control}
+ name="evaluationScore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가 점수</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min={0}
+ max={100}
+ placeholder="0-100점"
+ {...field}
+ value={field.value || ""}
+ onChange={(e) => {
+ const value = e.target.value === "" ? undefined : parseInt(e.target.value, 10)
+ field.onChange(value)
+ }}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 평가 결과 */}
+ <FormField
+ control={form.control}
+ name="evaluationResult"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가 결과</FormLabel>
+ <FormControl>
+ <Select value={field.value || ""} onValueChange={field.onChange}>
+ <SelectTrigger>
+ <SelectValue placeholder="평가 결과를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ <SelectItem value="APPROVED">승인</SelectItem>
+ <SelectItem value="SUPPLEMENT">보완</SelectItem>
+ <SelectItem value="SUPPLEMENT_REINSPECT">보완-재실사</SelectItem>
+ <SelectItem value="SUPPLEMENT_DOCUMENT">보완-서류제출</SelectItem>
+ <SelectItem value="REJECTED">불가</SelectItem>
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* QM 의견 */}
+ <FormField
+ control={form.control}
+ name="investigationNotes"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>QM 의견</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="실사에 대한 QM 의견을 입력하세요..."
+ {...field}
+ className="min-h-[80px]"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 파일 첨부 섹션 */}
+ {renderFileUploadSection()}
+ </form>
+ </Form>
+ </div>
+
+ {/* Footer Buttons */}
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0 flex-shrink-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline" disabled={isPending || uploadingFiles}>
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ disabled={isPending || uploadingFiles}
+ onClick={handleSaveClick}
+ >
+ {(isPending || uploadingFiles) && (
+ <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
+ )}
+ {uploadingFiles ? "업로드 중..." : isPending ? "저장 중..." : "저장"}
+ </Button>
+ </SheetFooter>
+ </SheetContent>
+ </Sheet>
+ )
+} \ No newline at end of file
diff --git a/lib/vendor-investigation/table/investigation-table-columns.tsx b/lib/vendor-investigation/table/investigation-table-columns.tsx
index b5344a1e..28ecc2ec 100644
--- a/lib/vendor-investigation/table/investigation-table-columns.tsx
+++ b/lib/vendor-investigation/table/investigation-table-columns.tsx
@@ -5,7 +5,14 @@ import { ColumnDef } from "@tanstack/react-table"
import { Checkbox } from "@/components/ui/checkbox"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
-import { Edit, Ellipsis } from "lucide-react"
+import { Edit, Ellipsis, AlertTriangle } from "lucide-react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
import { formatDate } from "@/lib/utils"
@@ -24,6 +31,7 @@ interface GetVendorInvestigationsColumnsProps {
>
>
openVendorDetailsModal?: (vendorId: number) => void
+ openSupplementRequestDialog?: (investigationId: number, investigationMethod: string, vendorName: string) => void
}
// Helper function for investigation method variants
@@ -45,6 +53,7 @@ function getMethodVariant(method: string): "default" | "secondary" | "outline" |
export function getColumns({
setRowAction,
openVendorDetailsModal,
+ openSupplementRequestDialog,
}: GetVendorInvestigationsColumnsProps): ColumnDef<
VendorInvestigationsViewWithContacts
>[] {
@@ -86,20 +95,69 @@ export function getColumns({
cell: ({ row }) => {
const isCanceled = row.original.investigationStatus === "CANCELED"
const isCompleted = row.original.investigationStatus === "COMPLETED"
+ const canRequestSupplement = (row.original.investigationMethod === "PRODUCT_INSPECTION" ||
+ row.original.investigationMethod === "SITE_VISIT_EVAL") &&
+ row.original.investigationStatus === "COMPLETED" &&
+ (row.original.evaluationResult === "SUPPLEMENT" ||
+ row.original.evaluationResult === "SUPPLEMENT_REINSPECT" ||
+ row.original.evaluationResult === "SUPPLEMENT_DOCUMENT")
+
return (
- <Button
- variant="ghost"
- className="flex size-8 p-0 data-[state=open]:bg-muted"
- aria-label="실사 정보 수정"
- disabled={isCanceled}
- onClick={() => {
- if (!isCanceled || !isCompleted) {
- setRowAction?.({ type: "update", row })
- }
- }}
- >
- <Edit className="size-4" aria-hidden="true" />
- </Button>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-48">
+ <DropdownMenuItem
+ onSelect={() => {
+ if (!isCanceled && row.original.investigationStatus === "PLANNED") {
+ setRowAction?.({ type: "update-progress", row })
+ }
+ }}
+ disabled={isCanceled || row.original.investigationStatus !== "PLANNED"}
+ >
+ <Edit className="mr-2 h-4 w-4" />
+ 실사 진행 관리
+ </DropdownMenuItem>
+
+ <DropdownMenuItem
+ onSelect={() => {
+ if (!isCanceled && row.original.investigationStatus === "IN_PROGRESS") {
+ setRowAction?.({ type: "update-result", row })
+ }
+ }}
+ disabled={isCanceled || row.original.investigationStatus !== "IN_PROGRESS"}
+ >
+ <Edit className="mr-2 h-4 w-4" />
+ 실사 결과 입력
+ </DropdownMenuItem>
+
+ {canRequestSupplement && (
+ <>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => {
+ openSupplementRequestDialog?.(
+ row.original.investigationId,
+ row.original.investigationMethod || "",
+ row.original.vendorName
+ )
+ }}
+ className="text-amber-600 focus:text-amber-600"
+ >
+ <AlertTriangle className="mr-2 h-4 w-4" />
+ 보완 요청
+ </DropdownMenuItem>
+ </>
+ )}
+ </DropdownMenuContent>
+ </DropdownMenu>
)
},
size: 40,
@@ -256,9 +314,9 @@ export function getColumns({
return (
<div className="flex flex-col">
<span>{value || "미배정"}</span>
- {row.original.requesterEmail && (
+ {row.original.requesterEmail ? (
<span className="text-xs text-muted-foreground">{row.original.requesterEmail}</span>
- )}
+ ) : null}
</div>
)
}
@@ -271,9 +329,9 @@ export function getColumns({
return (
<div className="flex flex-col">
<span>{value || "미배정"}</span>
- {row.original.qmManagerEmail && (
+ {row.original.qmManagerEmail ? (
<span className="text-xs text-muted-foreground">{row.original.qmManagerEmail}</span>
- )}
+ ) : null}
</div>
)
}
@@ -298,7 +356,7 @@ export function getColumns({
} else {
nestedColumns.push({
id: groupName,
- header: groupName,
+ header: groupName as any,
columns: colDefs,
})
}
@@ -325,6 +383,8 @@ function formatStatus(status: string): string {
return "완료됨"
case "CANCELED":
return "취소됨"
+ case "SUPPLEMENT_REQUIRED":
+ return "보완 요구됨"
case "RESULT_SENT":
return "실사결과발송"
default:
@@ -349,6 +409,10 @@ function formatEnumValue(value: string): string {
return "승인"
case "SUPPLEMENT":
return "보완"
+ case "SUPPLEMENT_REINSPECT":
+ return "보완-재실사"
+ case "SUPPLEMENT_DOCUMENT":
+ return "보완-서류제출"
case "REJECTED":
return "불가"
@@ -367,6 +431,10 @@ function getStatusVariant(status: string): "default" | "secondary" | "outline" |
return "outline"
case "CANCELED":
return "destructive"
+ case "SUPPLEMENT_REQUIRED":
+ return "secondary"
+ case "RESULT_SENT":
+ return "default"
default:
return "default"
}
@@ -380,6 +448,10 @@ function getResultVariant(result: string): "default" | "secondary" | "outline" |
return "default"
case "SUPPLEMENT":
return "secondary"
+ case "SUPPLEMENT_REINSPECT":
+ return "secondary"
+ case "SUPPLEMENT_DOCUMENT":
+ return "secondary"
case "REJECTED":
return "destructive"
default:
diff --git a/lib/vendor-investigation/table/investigation-table.tsx b/lib/vendor-investigation/table/investigation-table.tsx
index b7663629..ee122f04 100644
--- a/lib/vendor-investigation/table/investigation-table.tsx
+++ b/lib/vendor-investigation/table/investigation-table.tsx
@@ -16,8 +16,10 @@ import { getColumns } from "./investigation-table-columns"
import { getVendorsInvestigation } from "../service"
import { VendorsTableToolbarActions } from "./investigation-table-toolbar-actions"
import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig"
-import { UpdateVendorInvestigationSheet } from "./update-investigation-sheet"
+import { InvestigationResultSheet } from "./investigation-result-sheet"
+import { InvestigationProgressSheet } from "./investigation-progress-sheet"
import { VendorDetailsDialog } from "./vendor-details-dialog"
+import { SupplementRequestDialog } from "@/components/investigation/supplement-request-dialog"
interface VendorsTableProps {
promises: Promise<
@@ -54,12 +56,34 @@ export function VendorsInvestigationTable({ promises }: VendorsTableProps) {
const [vendorDetailsOpen, setVendorDetailsOpen] = React.useState(false)
const [selectedVendorId, setSelectedVendorId] = React.useState<number | null>(null)
+ // Add state for supplement request dialog
+ const [supplementRequestOpen, setSupplementRequestOpen] = React.useState(false)
+ const [supplementRequestData, setSupplementRequestData] = React.useState<{
+ investigationId: number
+ investigationMethod: string
+ vendorName: string
+ } | null>(null)
+
// Create handler for opening vendor details modal
const openVendorDetailsModal = React.useCallback((vendorId: number) => {
setSelectedVendorId(vendorId)
setVendorDetailsOpen(true)
}, [])
+ // Create handler for opening supplement request dialog
+ const openSupplementRequestDialog = React.useCallback((
+ investigationId: number,
+ investigationMethod: string,
+ vendorName: string
+ ) => {
+ setSupplementRequestData({
+ investigationId,
+ investigationMethod,
+ vendorName
+ })
+ setSupplementRequestOpen(true)
+ }, [])
+
// Get router
const router = useRouter()
@@ -67,9 +91,10 @@ export function VendorsInvestigationTable({ promises }: VendorsTableProps) {
const columns = React.useMemo(
() => getColumns({
setRowAction,
- openVendorDetailsModal
+ openVendorDetailsModal,
+ openSupplementRequestDialog
}),
- [setRowAction, openVendorDetailsModal]
+ [setRowAction, openVendorDetailsModal, openSupplementRequestDialog]
)
// 기본 필터 필드들
@@ -174,9 +199,15 @@ export function VendorsInvestigationTable({ promises }: VendorsTableProps) {
</DataTableAdvancedToolbar>
</DataTable>
- {/* Update Investigation Sheet */}
- <UpdateVendorInvestigationSheet
- open={rowAction?.type === "update"}
+ {/* Update Investigation Sheets */}
+ <InvestigationProgressSheet
+ open={rowAction?.type === "update-progress"}
+ onOpenChange={() => setRowAction(null)}
+ investigation={rowAction?.row.original ?? null}
+ />
+
+ <InvestigationResultSheet
+ open={rowAction?.type === "update-result"}
onOpenChange={() => setRowAction(null)}
investigation={rowAction?.row.original ?? null}
/>
@@ -187,6 +218,14 @@ export function VendorsInvestigationTable({ promises }: VendorsTableProps) {
onOpenChange={setVendorDetailsOpen}
vendorId={selectedVendorId}
/>
+
+ <SupplementRequestDialog
+ open={supplementRequestOpen}
+ onOpenChange={setSupplementRequestOpen}
+ investigationId={supplementRequestData?.investigationId || 0}
+ investigationMethod={supplementRequestData?.investigationMethod || ""}
+ vendorName={supplementRequestData?.vendorName || ""}
+ />
</>
)
} \ No newline at end of file
diff --git a/lib/vendor-investigation/table/update-investigation-sheet.tsx b/lib/vendor-investigation/table/update-investigation-sheet.tsx
index 9f7c8994..7daa9d44 100644
--- a/lib/vendor-investigation/table/update-investigation-sheet.tsx
+++ b/lib/vendor-investigation/table/update-investigation-sheet.tsx
@@ -107,7 +107,7 @@ const getFileUploadConfig = (status: string) => {
}
/**
- * 실사 정보 수정 시트
+ * 실사 결과 입력 시트
*/
export function UpdateVendorInvestigationSheet({
investigation,
@@ -539,11 +539,11 @@ export function UpdateVendorInvestigationSheet({
<Sheet {...props}>
<SheetContent className="flex flex-col h-full sm:max-w-xl" >
<SheetHeader className="text-left flex-shrink-0">
- <SheetTitle>실사 업데이트</SheetTitle>
+ <SheetTitle>실사 결과 입력</SheetTitle>
<SheetDescription>
{investigation?.vendorName && (
<span className="font-medium">{investigation.vendorName}</span>
- )}의 실사 정보를 수정합니다.
+ )}의 실사 결과를 입력합니다.
</SheetDescription>
</SheetHeader>
@@ -554,8 +554,8 @@ export function UpdateVendorInvestigationSheet({
className="flex flex-col gap-4"
id="update-investigation-form"
>
- {/* 실사 상태 */}
- <FormField
+ {/* 실사 상태 - 주석처리 (실사 결과 입력에서는 자동으로 완료됨/취소됨/보완요구됨으로 변경) */}
+ {/* <FormField
control={form.control}
name="investigationStatus"
render={({ field }) => (
@@ -572,6 +572,8 @@ export function UpdateVendorInvestigationSheet({
<SelectItem value="IN_PROGRESS">진행 중</SelectItem>
<SelectItem value="COMPLETED">완료됨</SelectItem>
<SelectItem value="CANCELED">취소됨</SelectItem>
+ <SelectItem value="SUPPLEMENT_REQUIRED">보완 요구됨</SelectItem>
+ <SelectItem value="RESULT_SENT">실사결과발송</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
@@ -579,10 +581,10 @@ export function UpdateVendorInvestigationSheet({
<FormMessage />
</FormItem>
)}
- />
+ /> */}
- {/* 실사 주소 */}
- <FormField
+ {/* 실사 주소 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
control={form.control}
name="investigationAddress"
render={({ field }) => (
@@ -598,10 +600,10 @@ export function UpdateVendorInvestigationSheet({
<FormMessage />
</FormItem>
)}
- />
+ /> */}
- {/* 실사 방법 */}
- <FormField
+ {/* 실사 방법 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
control={form.control}
name="investigationMethod"
render={({ field }) => (
@@ -625,10 +627,10 @@ export function UpdateVendorInvestigationSheet({
<FormMessage />
</FormItem>
)}
- />
+ /> */}
- {/* 실사 수행 예정일 */}
- <FormField
+ {/* 실사 수행 예정일 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
control={form.control}
name="forecastedAt"
render={({ field }) => (
@@ -662,10 +664,10 @@ export function UpdateVendorInvestigationSheet({
<FormMessage />
</FormItem>
)}
- />
+ /> */}
- {/* 실사 확정일 */}
- <FormField
+ {/* 실사 확정일 - 주석처리 (실사 진행 관리에서 처리) */}
+ {/* <FormField
control={form.control}
name="confirmedAt"
render={({ field }) => (
@@ -699,7 +701,7 @@ export function UpdateVendorInvestigationSheet({
<FormMessage />
</FormItem>
)}
- />
+ /> */}
{/* 실제 실사일 */}
<FormField
@@ -738,61 +740,59 @@ export function UpdateVendorInvestigationSheet({
)}
/>
- {/* 평가 점수 - 완료된 상태일 때만 표시 */}
- {form.watch("investigationStatus") === "COMPLETED" && (
- <FormField
- control={form.control}
- name="evaluationScore"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가 점수</FormLabel>
- <FormControl>
- <Input
- type="number"
- min={0}
- max={100}
- placeholder="0-100점"
- {...field}
- value={field.value || ""}
- onChange={(e) => {
- const value = e.target.value === "" ? undefined : parseInt(e.target.value, 10)
- field.onChange(value)
- }}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
+ {/* 평가 점수 */}
+ <FormField
+ control={form.control}
+ name="evaluationScore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가 점수</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min={0}
+ max={100}
+ placeholder="0-100점"
+ {...field}
+ value={field.value || ""}
+ onChange={(e) => {
+ const value = e.target.value === "" ? undefined : parseInt(e.target.value, 10)
+ field.onChange(value)
+ }}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
- {/* 평가 결과 - 완료된 상태일 때만 표시 */}
- {form.watch("investigationStatus") === "COMPLETED" && (
- <FormField
- control={form.control}
- name="evaluationResult"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가 결과</FormLabel>
- <FormControl>
- <Select value={field.value || ""} onValueChange={field.onChange}>
- <SelectTrigger>
- <SelectValue placeholder="평가 결과를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="APPROVED">승인</SelectItem>
- <SelectItem value="SUPPLEMENT">보완</SelectItem>
- <SelectItem value="REJECTED">불가</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
+ {/* 평가 결과 */}
+ <FormField
+ control={form.control}
+ name="evaluationResult"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가 결과</FormLabel>
+ <FormControl>
+ <Select value={field.value || ""} onValueChange={field.onChange}>
+ <SelectTrigger>
+ <SelectValue placeholder="평가 결과를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ <SelectItem value="APPROVED">승인</SelectItem>
+ <SelectItem value="SUPPLEMENT">보완</SelectItem>
+ <SelectItem value="SUPPLEMENT_REINSPECT">보완-재실사</SelectItem>
+ <SelectItem value="SUPPLEMENT_DOCUMENT">보완-서류제출</SelectItem>
+ <SelectItem value="REJECTED">불가</SelectItem>
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
{/* QM 의견 */}
<FormField