From 44b74ff4170090673b6eeacd8c528e0abf47b7aa Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 1 Dec 2025 19:52:06 +0900 Subject: (김준회) deprecated code 정리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfqs/table/ItemsDialog.tsx | 752 ----------------------------------------- 1 file changed, 752 deletions(-) delete mode 100644 lib/rfqs/table/ItemsDialog.tsx (limited to 'lib/rfqs/table/ItemsDialog.tsx') diff --git a/lib/rfqs/table/ItemsDialog.tsx b/lib/rfqs/table/ItemsDialog.tsx deleted file mode 100644 index 3d822499..00000000 --- a/lib/rfqs/table/ItemsDialog.tsx +++ /dev/null @@ -1,752 +0,0 @@ -"use client" - -import * as React from "react" -import { useForm, useFieldArray, useWatch } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@/components/ui/form" -import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" -import { - Command, - CommandInput, - CommandList, - CommandItem, - CommandGroup, - CommandEmpty -} from "@/components/ui/command" -import { Check, ChevronsUpDown, Plus, Trash2, Save, X, AlertCircle, Eye } from "lucide-react" -import { toast } from "sonner" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { Badge } from "@/components/ui/badge" - -import { createRfqItem, deleteRfqItem } from "../service" -import { RfqWithItemCount } from "@/db/schema/rfq" -import { RfqType } from "../validations" - -// Zod 스키마 - 수량은 string으로 받아서 나중에 변환 -const itemSchema = z.object({ - id: z.number().optional(), - itemCode: z.string().nonempty({ message: "아이템 코드를 선택해주세요" }), - description: z.string().optional(), - quantity: z.coerce.number().min(1, { message: "최소 수량은 1입니다" }).default(1), - uom: z.string().default("each"), -}); - -const itemsFormSchema = z.object({ - rfqId: z.number().int(), - items: z.array(itemSchema).min(1, { message: "최소 1개 이상의 아이템을 추가해주세요" }), -}); - -type ItemsFormSchema = z.infer; - -interface RfqsItemsDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - rfq: RfqWithItemCount | null; - defaultItems?: { - id?: number; - itemCode: string; - quantity?: number | null; - description?: string | null; - uom?: string | null; - }[]; - itemsList: { code: string | null; name: string }[]; - rfqType?: RfqType; -} - -export function RfqsItemsDialog({ - open, - onOpenChange, - rfq, - defaultItems = [], - itemsList, - rfqType -}: RfqsItemsDialogProps) { - const rfqId = rfq?.rfqId ?? 0; - - // 편집 가능 여부 확인 - DRAFT 상태일 때만 편집 가능 - const isEditable = rfq?.status === "DRAFT"; - - // 초기 아이템 ID 목록을 추적하기 위한 상태 추가 - const [initialItemIds, setInitialItemIds] = React.useState<(number | undefined)[]>([]); - - // 삭제된 아이템 ID를 저장하는 상태 추가 - const [deletedItemIds, setDeletedItemIds] = React.useState([]); - - // 1) form - const form = useForm({ - resolver: zodResolver(itemsFormSchema), - defaultValues: { - rfqId, - items: defaultItems.length > 0 ? defaultItems.map((it) => ({ - id: it.id, - quantity: it.quantity ?? 1, - uom: it.uom ?? "each", - itemCode: it.itemCode ?? "", - description: it.description ?? "", - })) : [{ itemCode: "", description: "", quantity: 1, uom: "each" }], - }, - mode: "onChange", // 입력 필드가 변경될 때마다 유효성 검사 - }); - - // 다이얼로그가 열릴 때마다 폼 초기화 및 초기 아이템 ID 저장 - React.useEffect(() => { - if (open) { - const initialItems = defaultItems.length > 0 - ? defaultItems.map((it) => ({ - id: it.id, - quantity: it.quantity ?? 1, - uom: it.uom ?? "each", - itemCode: it.itemCode ?? "", - description: it.description ?? "", - })) - : [{ itemCode: "", description: "", quantity: 1, uom: "each" }]; - - form.reset({ - rfqId, - items: initialItems, - }); - - // 초기 아이템 ID 목록 저장 - setInitialItemIds(defaultItems.map(item => item.id)); - - // 삭제된 아이템 목록 초기화 - setDeletedItemIds([]); - setHasUnsavedChanges(false); - } - }, [open, defaultItems, rfqId, form]); - - // 새로운 요소에 대한 ref 배열 - const inputRefs = React.useRef>([]); - const [isSubmitting, setIsSubmitting] = React.useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false); - const [isExitDialogOpen, setIsExitDialogOpen] = React.useState(false); - - // 폼 변경 감지 - 편집 가능한 경우에만 변경 감지 - React.useEffect(() => { - if (!isEditable) return; - - const subscription = form.watch(() => { - setHasUnsavedChanges(true); - }); - return () => subscription.unsubscribe(); - }, [form, isEditable]); - - // 2) field array - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "items", - }); - - // 3) watch items array - const watchItems = form.watch("items"); - - // 4) Add item row with auto-focus - function handleAddItem() { - if (!isEditable) return; - - // 명시적으로 숫자 타입으로 지정 - append({ - itemCode: "", - description: "", - quantity: 1, - uom: "each" - }); - setHasUnsavedChanges(true); - - // 다음 렌더링 사이클에서 새로 추가된 항목에 포커스 - setTimeout(() => { - const newIndex = fields.length; - const button = inputRefs.current[newIndex]; - if (button) { - button.click(); - } - }, 100); - } - - // 항목 직접 삭제 - 기존 ID가 있을 경우 삭제 목록에 추가 - const handleRemoveItem = (index: number) => { - if (!isEditable) return; - - const itemToRemove = form.getValues().items[index]; - - // 기존 ID가 있는 아이템이라면 삭제 목록에 추가 - if (itemToRemove.id !== undefined) { - setDeletedItemIds(prev => [...prev, itemToRemove.id as number]); - } - - remove(index); - setHasUnsavedChanges(true); - - // 포커스 처리: 다음 항목이 있으면 다음 항목으로, 없으면 마지막 항목으로 - setTimeout(() => { - const nextIndex = Math.min(index, fields.length - 1); - if (nextIndex >= 0 && inputRefs.current[nextIndex]) { - inputRefs.current[nextIndex]?.click(); - } - }, 50); - }; - - // 다이얼로그 닫기 전 확인 - const handleDialogClose = (open: boolean) => { - if (!open && hasUnsavedChanges && isEditable) { - setIsExitDialogOpen(true); - } else { - onOpenChange(open); - } - }; - - // 필드 포커스 유틸리티 함수 - const focusField = (selector: string) => { - if (!isEditable) return; - - setTimeout(() => { - const element = document.querySelector(selector) as HTMLInputElement | null; - if (element) { - element.focus(); - } - }, 10); - }; - - // 5) Submit - 업데이트된 제출 로직 (생성/수정 + 삭제 처리) - async function onSubmit(data: ItemsFormSchema) { - if (!isEditable) return; - - try { - setIsSubmitting(true); - - // 각 아이템이 유효한지 확인 - const anyInvalidItems = data.items.some(item => !item.itemCode || item.quantity < 1); - - if (anyInvalidItems) { - toast.error("유효하지 않은 아이템이 있습니다. 모든 필드를 확인해주세요."); - setIsSubmitting(false); - return; - } - - // 1. 삭제 처리 - 삭제된 아이템 ID가 있으면 삭제 요청 - const deletePromises = deletedItemIds.map(id => - deleteRfqItem({ - id: id, - rfqId: rfqId, - rfqType: rfqType ?? RfqType.PURCHASE - }) - ); - - // 2. 생성/수정 처리 - 폼에 남아있는 아이템들 - const upsertPromises = data.items.map((item) => - createRfqItem({ - rfqId: rfqId, - itemCode: item.itemCode, - description: item.description, - // 명시적으로 숫자로 변환 - quantity: Number(item.quantity), - uom: item.uom, - rfqType: rfqType ?? RfqType.PURCHASE, - id: item.id // 기존 ID가 있으면 업데이트, 없으면 생성 - }) - ); - - // 모든 요청 병렬 처리 - await Promise.all([...deletePromises, ...upsertPromises]); - - toast.success("RFQ 아이템이 성공적으로 저장되었습니다!"); - setHasUnsavedChanges(false); - onOpenChange(false); - } catch (err) { - toast.error(`오류가 발생했습니다: ${String(err)}`); - } finally { - setIsSubmitting(false); - } - } - - // 단축키 처리 - 편집 가능한 경우에만 단축키 활성화 - React.useEffect(() => { - if (!isEditable) return; - - const handleKeyDown = (e: KeyboardEvent) => { - // Alt+N: 새 항목 추가 - if (e.altKey && e.key === 'n') { - e.preventDefault(); - handleAddItem(); - } - // Ctrl+S: 저장 - if ((e.ctrlKey || e.metaKey) && e.key === 's') { - e.preventDefault(); - form.handleSubmit(onSubmit)(); - } - // Esc: 포커스된 팝오버 닫기 - if (e.key === 'Escape') { - document.querySelectorAll('[role="combobox"][aria-expanded="true"]').forEach( - (el) => (el as HTMLButtonElement).click() - ); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [form, isEditable]); - - return ( - <> - - - - - {isEditable ? "RFQ 아이템 관리" : "RFQ 아이템 조회"} - - {rfq?.rfqCode || `RFQ #${rfqId}`} - - {rfqType && ( - - {rfqType === RfqType.PURCHASE ? "구매 RFQ" : "예산 RFQ"} - - )} - {rfq?.status && ( - - {rfq.status} - - )} - - - {isEditable - ? (rfq?.description || '아이템을 각 행에 하나씩 추가할 수 있습니다.') - : '드래프트 상태가 아닌 RFQ는 아이템을 편집할 수 없습니다.'} - - -
-
- -
- {/* 헤더 행 (라벨) */} -
-
아이템
-
설명
-
수량
-
단위
- {isEditable &&
} -
- - {/* 아이템 행들 */} -
- {fields.map((field, index) => { - // 현재 row의 itemCode - const codeValue = watchItems[index]?.itemCode || ""; - // "이미" 사용된 코드를 모두 구함 - const usedCodes = watchItems - .map((it, i) => i === index ? null : it.itemCode) - .filter(Boolean) as string[]; - - // itemsList에서 "현재 선택한 code"만 예외적으로 허용하고, - // 다른 행에서 이미 사용한 code는 제거 - const filteredItems = (itemsList || []) - .filter((it) => { - if (!it.code) return false; - if (it.code === codeValue) return true; - return !usedCodes.includes(it.code); - }) - .map((it) => ({ - code: it.code ?? "", // fallback - name: it.name, - })); - - // 선택된 아이템 찾기 - const selected = filteredItems.find(it => it.code === codeValue); - - return ( -
- {/* -- itemCode + Popover(Select) -- */} - {isEditable ? ( - // 전체 FormField 컴포넌트와 아이템 선택 로직 개선 - { - const [popoverOpen, setPopoverOpen] = React.useState(false); - const selected = filteredItems.find(it => it.code === field.value); - - return ( - - - - - - - - - - - 아이템을 찾을 수 없습니다. - - {filteredItems.map((it) => { - const label = `${it.code} - ${it.name}`; - return ( - { - field.onChange(it.code); - setPopoverOpen(false); - // 자동으로 다음 필드로 포커스 이동 - focusField(`input[name="items.${index}.description"]`); - }} - > -
- {label} -
- -
- ); - })} -
-
-
-
-
-
- {form.formState.errors.items?.[index]?.itemCode && ( - - )} -
- ); - }} - /> - ) : ( -
- {selected ? `${selected.code} - ${selected.name}` : codeValue} -
- )} - - {/* ID 필드 추가 (숨김) */} - ( - - )} - /> - - {/* description */} - {isEditable ? ( - ( - - - { - if (e.key === 'Enter') { - e.preventDefault(); - focusField(`input[name="items.${index}.quantity"]`); - } - }} - /> - - - - )} - /> - ) : ( -
- {watchItems[index]?.description || ""} -
- )} - - {/* quantity */} - {isEditable ? ( - ( - - - { - const value = e.target.value === '' ? 1 : parseInt(e.target.value, 10); - field.onChange(isNaN(value) ? 1 : value); - }} - // 최소값 보장 (빈 문자열 방지) - onBlur={(e) => { - if (e.target.value === '' || parseInt(e.target.value, 10) < 1) { - field.onChange(1); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - focusField(`input[name="items.${index}.uom"]`); - } - }} - /> - - {form.formState.errors.items?.[index]?.quantity && ( - - )} - - )} - /> - ) : ( -
- {watchItems[index]?.quantity} -
- )} - - {/* uom */} - {isEditable ? ( - ( - - - { - if (e.key === 'Enter') { - e.preventDefault(); - // 마지막 행이면 새로운 행 추가 - if (index === fields.length - 1) { - handleAddItem(); - } else { - // 아니면 다음 행의 아이템 선택으로 이동 - const button = inputRefs.current[index + 1]; - if (button) { - setTimeout(() => button.click(), 10); - } - } - } - }} - /> - - - - )} - /> - ) : ( -
- {watchItems[index]?.uom || "each"} -
- )} - - {/* remove row - 편집 모드에서만 표시 */} - {isEditable && ( - - - - - - -

아이템 삭제

-
-
-
- )} -
- ); - })} -
- -
-
- {isEditable ? ( - <> - - - - - - -

단축키: Alt+N

-
-
-
- - {fields.length}개 아이템 - - {deletedItemIds.length > 0 && ( - - ({deletedItemIds.length}개 아이템 삭제 예정) - - )} - - ) : ( - - {fields.length}개 아이템 - - )} -
- - {isEditable && ( -
- - Tab - 필드 간 이동 - - - Enter - 다음 필드로 이동 - -
- )} -
-
- - - {isEditable ? ( - <> - - - - - - 변경사항을 저장하지 않고 나가기 - - - - - - - - - -

단축키: Ctrl+S

-
-
-
- - ) : ( - - )} -
-
- -
-
-
- - {/* 저장하지 않고 나가기 확인 다이얼로그 - 편집 모드에서만 활성화 */} - {isEditable && ( - - - - 저장되지 않은 변경사항 - - 저장되지 않은 변경사항이 있습니다. 그래도 나가시겠습니까? - - - - 취소 - { - setIsExitDialogOpen(false); - onOpenChange(false); - }}> - 저장하지 않고 나가기 - - - - - )} - - ); -} \ No newline at end of file -- cgit v1.2.3