diff --git a/react/src/features/tag/components/TagAddModal.tsx b/react/src/features/tag/components/TagAddModal.tsx new file mode 100644 index 0000000..4bd9200 --- /dev/null +++ b/react/src/features/tag/components/TagAddModal.tsx @@ -0,0 +1,290 @@ +import { useState, useRef, useEffect } from "react"; +import useShake from "@/hooks/useShake"; +import { SERVICE_OPTIONS } from "../types"; + +interface TagAddModalProps { + open: boolean; + onClose: () => void; + onSave: (data: { name: string; service: string; description: string }) => void; +} + +export default function TagAddModal({ open, onClose, onSave }: TagAddModalProps) { + const { triggerShake, cls } = useShake(); + + const [name, setName] = useState(""); + const [service, setService] = useState(""); + const [description, setDescription] = useState(""); + const [serviceOpen, setServiceOpen] = useState(false); + const [confirmOpen, setConfirmOpen] = useState(false); + + // 에러 상태 + const [nameError, setNameError] = useState(false); + const [serviceError, setServiceError] = useState(false); + + const serviceRef = useRef(null); + + // 외부 클릭 시 서비스 드롭다운 닫기 + useEffect(() => { + function handleClick(e: MouseEvent) { + if (serviceRef.current && !serviceRef.current.contains(e.target as Node)) { + setServiceOpen(false); + } + } + document.addEventListener("click", handleClick); + return () => document.removeEventListener("click", handleClick); + }, []); + + const resetForm = () => { + setName(""); + setService(""); + setDescription(""); + setNameError(false); + setServiceError(false); + setServiceOpen(false); + setConfirmOpen(false); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const handleSaveClick = () => { + const errors: string[] = []; + if (!name.trim()) { + setNameError(true); + errors.push("name"); + } + if (!service) { + setServiceError(true); + errors.push("service"); + } + if (errors.length > 0) { + triggerShake(errors); + return; + } + // 확인 모달 열기 + setConfirmOpen(true); + }; + + const handleConfirmSave = () => { + onSave({ name: name.trim(), service, description: description.trim() }); + resetForm(); + onClose(); + }; + + if (!open) return null; + + return ( + <> + {/* 태그 추가 모달 */} +
{ + if (e.target === e.currentTarget) handleClose(); + }} + > +
+
+ {/* 헤더 */} +
+
+ + sell + +
+

태그 추가

+
+ + {/* 바디 */} +
+ {/* 태그명 */} +
+ + { + setName(e.target.value); + if (e.target.value.trim()) setNameError(false); + }} + placeholder="태그 이름을 입력하세요" + className={`w-full h-[38px] px-3 border rounded-lg text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all ${ + nameError + ? "border-red-500 shadow-[0_0_0_2px_rgba(239,68,68,0.15)]" + : "border-gray-300" + } ${cls("name")}`} + /> + {nameError && ( +
+ + error + + 필수 입력 항목입니다. +
+ )} +
+ + {/* 서비스 선택 */} +
+ +
+ + {serviceOpen && ( +
    + {SERVICE_OPTIONS.map((opt) => ( +
  • { + setService(opt); + setServiceError(false); + setServiceOpen(false); + }} + className={`px-3 py-2 text-sm hover:bg-gray-50 cursor-pointer transition-colors ${ + opt === service ? "text-primary font-medium" : "" + }`} + > + {opt} +
  • + ))} +
+ )} +
+ {serviceError && ( +
+ + error + + 필수 입력 항목입니다. +
+ )} +
+ + {/* 설명 */} +
+ +