summaryrefslogtreecommitdiff
path: root/lib/rfqs/table/ItemsDialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfqs/table/ItemsDialog.tsx')
-rw-r--r--lib/rfqs/table/ItemsDialog.tsx744
1 files changed, 744 insertions, 0 deletions
diff --git a/lib/rfqs/table/ItemsDialog.tsx b/lib/rfqs/table/ItemsDialog.tsx
new file mode 100644
index 00000000..f1dbf90e
--- /dev/null
+++ b/lib/rfqs/table/ItemsDialog.tsx
@@ -0,0 +1,744 @@
+"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<typeof itemsFormSchema>;
+
+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<number[]>([]);
+
+ // 1) form
+ const form = useForm<ItemsFormSchema>({
+ 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<Array<HTMLButtonElement | null>>([]);
+ 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 (
+ <>
+ <Dialog open={open} onOpenChange={handleDialogClose}>
+ <DialogContent className="max-w-none w-[1200px]">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ {isEditable ? "RFQ 아이템 관리" : "RFQ 아이템 조회"}
+ <Badge variant="outline" className="ml-2">
+ {rfq?.rfqCode || `RFQ #${rfqId}`}
+ </Badge>
+ {rfqType && (
+ <Badge variant={rfqType === RfqType.PURCHASE ? "default" : "secondary"} className="ml-1">
+ {rfqType === RfqType.PURCHASE ? "구매 RFQ" : "예산 RFQ"}
+ </Badge>
+ )}
+ {rfq?.status && (
+ <Badge
+ variant={rfq.status === "DRAFT" ? "outline" : "secondary"}
+ className="ml-1"
+ >
+ {rfq.status}
+ </Badge>
+ )}
+ </DialogTitle>
+ <DialogDescription>
+ {isEditable
+ ? (rfq?.description || '아이템을 각 행에 하나씩 추가할 수 있습니다.')
+ : '드래프트 상태가 아닌 RFQ는 아이템을 편집할 수 없습니다.'}
+ </DialogDescription>
+ </DialogHeader>
+ <div className="overflow-x-auto w-full">
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)}>
+ <div className="space-y-4">
+ {/* 헤더 행 (라벨) */}
+ <div className="flex items-center gap-2 border-b pb-2 font-medium text-sm">
+ <div className="w-[250px] pl-3">아이템</div>
+ <div className="w-[400px] pl-2">설명</div>
+ <div className="w-[80px] pl-2 text-center">수량</div>
+ <div className="w-[80px] pl-2 text-center">단위</div>
+ {isEditable && <div className="w-[42px]"></div>}
+ </div>
+
+ {/* 아이템 행들 */}
+ <div className="max-h-[50vh] overflow-y-auto pr-1 space-y-3">
+ {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 (
+ <div key={field.id} className="flex items-center gap-2 group hover:bg-gray-50 p-1 rounded-md transition-colors">
+ {/* -- itemCode + Popover(Select) -- */}
+ {isEditable ? (
+ <FormField
+ control={form.control}
+ name={`items.${index}.itemCode`}
+ render={({ field }) => {
+ const [popoverOpen, setPopoverOpen] = React.useState(false);
+ const selected = filteredItems.find(it => it.code === field.value);
+
+ return (
+ <FormItem className="flex items-center gap-2 w-[250px]">
+ <FormControl>
+ <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ // 컴포넌트에 ref 전달
+ ref={el => {
+ inputRefs.current[index] = el;
+ }}
+ variant="outline"
+ role="combobox"
+ aria-expanded={popoverOpen}
+ className="w-full justify-between"
+ data-error={!!form.formState.errors.items?.[index]?.itemCode}
+ data-state={selected ? "filled" : "empty"}
+ >
+ {selected ? `${selected.code} - ${selected.name}` : "아이템 선택..."}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput placeholder="아이템 검색..." className="h-9" autoFocus />
+ <CommandList>
+ <CommandEmpty>아이템을 찾을 수 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {filteredItems.map((it) => {
+ const label = `${it.code} - ${it.name}`;
+ return (
+ <CommandItem
+ key={it.code}
+ value={label}
+ onSelect={() => {
+ field.onChange(it.code);
+ setPopoverOpen(false);
+ // 자동으로 다음 필드로 포커스 이동
+ focusField(`input[name="items.${index}.description"]`);
+ }}
+ >
+ {label}
+ <Check
+ className={
+ "ml-auto h-4 w-4" +
+ (it.code === field.value ? " opacity-100" : " opacity-0")
+ }
+ />
+ </CommandItem>
+ );
+ })}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ </FormControl>
+ {form.formState.errors.items?.[index]?.itemCode && (
+ <AlertCircle className="h-4 w-4 text-destructive" />
+ )}
+ </FormItem>
+ );
+ }}
+ />
+ ) : (
+ <div className="flex items-center w-[250px] pl-3">
+ {selected ? `${selected.code} - ${selected.name}` : codeValue}
+ </div>
+ )}
+
+ {/* ID 필드 추가 (숨김) */}
+ <FormField
+ control={form.control}
+ name={`items.${index}.id`}
+ render={({ field }) => (
+ <input type="hidden" {...field} />
+ )}
+ />
+
+ {/* description */}
+ {isEditable ? (
+ <FormField
+ control={form.control}
+ name={`items.${index}.description`}
+ render={({ field }) => (
+ <FormItem className="w-[400px]">
+ <FormControl>
+ <Input
+ className="w-full"
+ placeholder="아이템 상세 정보"
+ {...field}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ focusField(`input[name="items.${index}.quantity"]`);
+ }
+ }}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ ) : (
+ <div className="w-[400px] pl-2">
+ {watchItems[index]?.description || ""}
+ </div>
+ )}
+
+ {/* quantity */}
+ {isEditable ? (
+ <FormField
+ control={form.control}
+ name={`items.${index}.quantity`}
+ render={({ field }) => (
+ <FormItem className="w-[80px] relative">
+ <FormControl>
+ <Input
+ type="number"
+ className="w-full text-center"
+ min="1"
+ {...field}
+ // 값 변경 핸들러 개선
+ onChange={(e) => {
+ 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"]`);
+ }
+ }}
+ />
+ </FormControl>
+ {form.formState.errors.items?.[index]?.quantity && (
+ <AlertCircle className="h-4 w-4 text-destructive absolute right-2 top-2" />
+ )}
+ </FormItem>
+ )}
+ />
+ ) : (
+ <div className="w-[80px] text-center">
+ {watchItems[index]?.quantity}
+ </div>
+ )}
+
+ {/* uom */}
+ {isEditable ? (
+ <FormField
+ control={form.control}
+ name={`items.${index}.uom`}
+ render={({ field }) => (
+ <FormItem className="w-[80px]">
+ <FormControl>
+ <Input
+ placeholder="each"
+ className="w-full text-center"
+ {...field}
+ onKeyDown={(e) => {
+ 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);
+ }
+ }
+ }
+ }}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ ) : (
+ <div className="w-[80px] text-center">
+ {watchItems[index]?.uom || "each"}
+ </div>
+ )}
+
+ {/* remove row - 편집 모드에서만 표시 */}
+ {isEditable && (
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ onClick={() => handleRemoveItem(index)}
+ className="group-hover:opacity-100 transition-opacity"
+ aria-label="아이템 삭제"
+ >
+ <Trash2 className="h-4 w-4 text-destructive" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>아이템 삭제</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ )}
+ </div>
+ );
+ })}
+ </div>
+
+ <div className="flex justify-between items-center pt-2 border-t">
+ <div className="flex items-center gap-2">
+ {isEditable ? (
+ <>
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button type="button" variant="outline" onClick={handleAddItem} className="gap-1">
+ <Plus className="h-4 w-4" />
+ 아이템 추가
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent side="bottom">
+ <p>단축키: Alt+N</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ <span className="text-sm text-muted-foreground">
+ {fields.length}개 아이템
+ </span>
+ {deletedItemIds.length > 0 && (
+ <span className="text-sm text-destructive">
+ ({deletedItemIds.length}개 아이템 삭제 예정)
+ </span>
+ )}
+ </>
+ ) : (
+ <span className="text-sm text-muted-foreground">
+ {fields.length}개 아이템
+ </span>
+ )}
+ </div>
+
+ {isEditable && (
+ <div className="text-xs text-muted-foreground">
+ <span className="inline-flex items-center gap-1 mr-2">
+ <kbd className="px-1 py-0.5 bg-gray-100 rounded text-xs">Tab</kbd>
+ <span>필드 간 이동</span>
+ </span>
+ <span className="inline-flex items-center gap-1">
+ <kbd className="px-1 py-0.5 bg-gray-100 rounded text-xs">Enter</kbd>
+ <span>다음 필드로 이동</span>
+ </span>
+ </div>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter className="mt-6 gap-2">
+ {isEditable ? (
+ <>
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button type="button" variant="outline" onClick={() => handleDialogClose(false)}>
+ <X className="mr-2 h-4 w-4" />
+ 취소
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>변경사항을 저장하지 않고 나가기</TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ type="submit"
+ disabled={isSubmitting || (!form.formState.isDirty && deletedItemIds.length === 0) || !form.formState.isValid}
+ >
+ {isSubmitting ? (
+ <>처리 중...</>
+ ) : (
+ <>
+ <Save className="mr-2 h-4 w-4" />
+ 저장
+ </>
+ )}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>단축키: Ctrl+S</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </>
+ ) : (
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
+ <X className="mr-2 h-4 w-4" />
+ 닫기
+ </Button>
+ )}
+ </DialogFooter>
+ </form>
+ </Form>
+ </div>
+ </DialogContent>
+ </Dialog>
+
+ {/* 저장하지 않고 나가기 확인 다이얼로그 - 편집 모드에서만 활성화 */}
+ {isEditable && (
+ <AlertDialog open={isExitDialogOpen} onOpenChange={setIsExitDialogOpen}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>저장되지 않은 변경사항</AlertDialogTitle>
+ <AlertDialogDescription>
+ 저장되지 않은 변경사항이 있습니다. 그래도 나가시겠습니까?
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogAction onClick={() => {
+ setIsExitDialogOpen(false);
+ onOpenChange(false);
+ }}>
+ 저장하지 않고 나가기
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )}
+ </>
+ );
+} \ No newline at end of file