From 8a19a6fa336768d8b6712752c9d713360067ecb0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 8 Dec 2025 08:45:20 +0000 Subject: (최겸) 구매 피드백 수정, 안전담당자, pq항목 내 첨부, 내외자 구분, 도로명주소 api 반영(운영기준) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pq/pq-criteria/add-pq-dialog.tsx | 145 +++++++++++++++++++++++++++++++- lib/pq/pq-criteria/update-pq-sheet.tsx | 148 ++++++++++++++++++++++++++++++++- 2 files changed, 289 insertions(+), 4 deletions(-) (limited to 'lib/pq/pq-criteria') diff --git a/lib/pq/pq-criteria/add-pq-dialog.tsx b/lib/pq/pq-criteria/add-pq-dialog.tsx index 660eb360..1752f503 100644 --- a/lib/pq/pq-criteria/add-pq-dialog.tsx +++ b/lib/pq/pq-criteria/add-pq-dialog.tsx @@ -38,6 +38,10 @@ import { import { useToast } from "@/hooks/use-toast" import { createPqCriteria } from "../service" +import { uploadPqCriteriaFileAction } from "@/lib/pq/service" +import { Dropzone, DropzoneInput, DropzoneZone, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription } from "@/components/ui/dropzone" +import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction } from "@/components/ui/file-list" +import { X, Loader2 } from "lucide-react" // PQ 생성을 위한 Zod 스키마 정의 const createPqSchema = z.object({ @@ -48,7 +52,7 @@ const createPqSchema = z.object({ description: z.string().optional(), remarks: z.string().optional(), inputFormat: z.string().default("TEXT"), - + type: z.string().optional(), }); type CreatePqFormType = z.infer; @@ -74,6 +78,12 @@ const inputFormatOptions = [ { value: "TEXT_FILE", label: "텍스트 + 파일" }, ]; +const typeOptions = [ + { value: "내자", label: "내자" }, + { value: "외자", label: "외자" }, + { value: "내외자", label: "내외자" }, +]; + interface AddPqDialogProps { pqListId: number; } @@ -81,6 +91,10 @@ interface AddPqDialogProps { export function AddPqDialog({ pqListId }: AddPqDialogProps) { const [open, setOpen] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) + const [isUploading, setIsUploading] = React.useState(false) + const [uploadedFiles, setUploadedFiles] = React.useState< + { fileName: string; url: string; size?: number; originalFileName?: string }[] + >([]) const router = useRouter() const { toast } = useToast() @@ -95,7 +109,7 @@ export function AddPqDialog({ pqListId }: AddPqDialogProps) { description: "", remarks: "", inputFormat: "TEXT", - + type: "내외자", }, }) const formState = form.formState @@ -105,7 +119,10 @@ export function AddPqDialog({ pqListId }: AddPqDialogProps) { setIsSubmitting(true) // 서버 액션 호출 - const result = await createPqCriteria(pqListId, data) + const result = await createPqCriteria(pqListId, { + ...data, + attachments: uploadedFiles, + }) if (!result.success) { toast({ @@ -124,6 +141,7 @@ export function AddPqDialog({ pqListId }: AddPqDialogProps) { // 모달 닫고 폼 리셋 form.reset() + setUploadedFiles([]) setOpen(false) // 페이지 새로고침 @@ -144,10 +162,34 @@ export function AddPqDialog({ pqListId }: AddPqDialogProps) { function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { form.reset() + setUploadedFiles([]) } setOpen(nextOpen) } + const handleUpload = async (files: File[]) => { + try { + setIsUploading(true) + for (const file of files) { + const uploaded = await uploadPqCriteriaFileAction(file) + setUploadedFiles((prev) => [...prev, uploaded]) + } + toast({ + title: "업로드 완료", + description: "첨부파일이 업로드되었습니다.", + }) + } catch (error) { + console.error(error) + toast({ + title: "업로드 실패", + description: "첨부파일 업로드 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsUploading(false) + } + } + return ( @@ -253,6 +295,33 @@ export function AddPqDialog({ pqListId }: AddPqDialogProps) { )} /> + {/* Type 필드 */} + ( + + 내/외자 구분 + + 미선택 시 기본값은 내외자입니다. + + + )} + /> + {/* Input Format 필드 */} + {/* 첨부 파일 업로드 */} +
+
+ 첨부 파일 + {isUploading && ( +
+ 업로드 중... +
+ )} +
+ handleUpload(files)} + onDropRejected={() => + toast({ + title: "업로드 실패", + description: "파일 크기/형식을 확인하세요.", + variant: "destructive", + }) + } + disabled={isUploading} + > + {() => ( + + + + + +
+ +
+ 파일을 드래그하거나 클릭하여 업로드 + PDF, 이미지, 문서 (최대 600MB) +
+
+
+ 기준 문서 첨부가 필요한 경우 업로드하세요. +
+ )} +
+ + {uploadedFiles.length > 0 && ( +
+

첨부된 파일 ({uploadedFiles.length})

+ + {uploadedFiles.map((file, idx) => ( + + + + {file.originalFileName || file.fileName} + {file.size && ( + {`${file.size} bytes`} + )} + + + setUploadedFiles((prev) => prev.filter((_, i) => i !== idx)) + } + > + + Remove + + + + ))} + +
+ )} +
+ {/* Description 필드 */} ; @@ -67,6 +72,12 @@ const inputFormatOptions = [ { value: "TEXT_FILE", label: "텍스트 + 파일" } ]; +const typeOptions = [ + { value: "내자", label: "내자" }, + { value: "외자", label: "외자" }, + { value: "내외자", label: "내외자" }, +]; + interface UpdatePqSheetProps extends React.ComponentPropsWithRef { pq: { @@ -79,11 +90,16 @@ interface UpdatePqSheetProps inputFormat: string; subGroupName: string | null; + type?: string | null; } | null } export function UpdatePqSheet({ pq, ...props }: UpdatePqSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() + const [isUploading, setIsUploading] = React.useState(false) + const [attachments, setAttachments] = React.useState< + { fileName: string; url: string; size?: number; originalFileName?: string }[] + >([]) const router = useRouter() const form = useForm({ @@ -97,6 +113,7 @@ export function UpdatePqSheet({ pq, ...props }: UpdatePqSheetProps) { inputFormat: pq?.inputFormat ?? "TEXT", subGroupName: pq?.subGroupName ?? "", + type: pq?.type ?? "내외자", }, }) @@ -112,15 +129,51 @@ export function UpdatePqSheet({ pq, ...props }: UpdatePqSheetProps) { inputFormat: pq.inputFormat ?? "TEXT", subGroupName: pq.subGroupName ?? "", + type: pq.type ?? "내외자", }); + + // 기존 첨부 로드 + getPqCriteriaAttachments(pq.id).then((res) => { + if (res.success && res.data) { + setAttachments( + res.data.map((a) => ({ + fileName: a.fileName, + url: a.filePath, + size: a.fileSize ?? undefined, + originalFileName: a.originalFileName || a.fileName, + })) + ) + } else { + setAttachments([]) + } + }) } }, [pq, form]); + const handleUpload = async (files: File[]) => { + try { + setIsUploading(true) + for (const file of files) { + const uploaded = await uploadPqCriteriaFileAction(file) + setAttachments((prev) => [...prev, uploaded]) + } + toast.success("첨부파일이 업로드되었습니다") + } catch (error) { + console.error(error) + toast.error("첨부파일 업로드에 실패했습니다") + } finally { + setIsUploading(false) + } + } + function onSubmit(input: UpdatePqSchema) { startUpdateTransition(async () => { if (!pq) return - const result = await updatePqCriteria(pq.id, input) + const result = await updatePqCriteria(pq.id, { + ...input, + attachments, + }) if (!result.success) { toast.error(result.message || "PQ 항목 수정에 실패했습니다") @@ -231,6 +284,33 @@ export function UpdatePqSheet({ pq, ...props }: UpdatePqSheetProps) { )} /> + {/* Type 필드 */} + ( + + 내/외자 구분 + + 미선택 시 기본값은 내외자입니다. + + + )} + /> + {/* Input Format 필드 */} + {/* 첨부 파일 업로드 */} +
+
+ 첨부 파일 + {isUploading && ( +
+ 업로드 중... +
+ )} +
+ handleUpload(files)} + onDropRejected={() => + toast.error("파일 크기/형식을 확인하세요.") + } + disabled={isUploading} + > + {() => ( + + + + + +
+ +
+ 파일을 드래그하거나 클릭하여 업로드 + PDF, 이미지, 문서 (최대 600MB) +
+
+
+ 기준 문서 첨부가 필요한 경우 업로드하세요. +
+ )} +
+ + {attachments.length > 0 && ( +
+

첨부된 파일 ({attachments.length})

+ + {attachments.map((file, idx) => ( + + + + {file.originalFileName || file.fileName} + {file.size && ( + {`${file.size} bytes`} + )} + + + setAttachments((prev) => prev.filter((_, i) => i !== idx)) + } + > + + Remove + + + + ))} + +
+ )} +
+ {/* Required 체크박스 */} -- cgit v1.2.3