목차


사전 분석

<aside>

[ui]

  1. 필요 컴포넌트
    1. input - text
    2. textarea
    3. input - file (?)
      1. div 글자 설정
      2. x 버튼
    4. selectBox → div로 구현
    5. errorMessage
    6. button (팀원이 만들어 줌)
  2. 조합
    1. css 설정

[기능 구현]

  1. 유효성 검사: 오류 났을 때 오류 메시지 보이게 처리 (메시지 종류 多)
    1. [text 전체] 입력 안 하면 못 넘어가게 함 (설명 부분 제외)
    2. [등급/장르] 기본 설정 없음, 선택하게 함
    3. [사진] 안 넣으면 못 넘어가게 함
    4. [발행량] 10장 이하
    5. [가격] 숫자만 입력되게
  2. value로 입력 값 저장
  3. 이미지 올리는 처리 (원래 multer로 작업했으나 성훈 님이 cloudnary로 바꿈)
  4. 제출: 버튼 누르거나 enter key 누를 시. </aside>

작업

< usePostForm >

<aside>

[상태 목록]

  1. form - name, grade, genre, price, volumn, image, description
  2. errors → 불안해서 newError 객체를 만들고 그 안에서 오류 감지하게 함
  3. isValid (유효성 검사: 버튼에서 씀, 검사 통과 못하면 안 넘어가게 함)
  4. isSubmitted (제출 여부 확인)
  5. dirty (각 영역에서 입력 여부 확인)
  6. isPending, isError (react client에서)

[함수 목록]

  1. openModal = 만든 거 불러옴
  2. handleChange (입력 상태 감지)
    1. handleFileChange
    2. handleSelectChange
  3. validate (내보내지는 않고 자체 유효성 검사 처리)
  4. ✅ useMutation (handleSubmit에 묶어서 내보냄)
  5. handleSubmit (제출 함수) </aside>
export default function usePostForm() {
  // ✅ 상태 목록
  const [form, setForm] = useState({
    name: "", grade: "", genre: "", price: "", volumn: "", image: "", description: "",
  });

  const [errors, setErrors] = useState({});
  const [isValid, setIsValid] = useState(false);
  const [isSubmitted, setIsSubmitted] = useState(false); // 제출 여부(오류 검사 때문에 만듦)

  const [dirty, setDirty] = useState({
    name: false, grade: false, genre: false, price: false, volumn: false, description: false,
  }); // 입력 여부(오류 검사 때문에 만듦22) - 관행적으로 "입력된 상태"를 "dirty"라고 함.

	// 모달 열기 + queryClient
  const { openModal, isOpen } = useStateModal(); // 모달 provider
  const queryClient = useQueryClient();

  // ✅ 버튼 누르면 초기화
  const resetForm = () => {
    setForm({
      name: "", grade: "",genre: "", price: "", volumn: "", image: "", description: "",
    });
    setDirty({
      name: false, grade: false, genre: false, price: false, volumn: false, description: false,
    });
    setIsSubmitted(false);
  };

  // ✅ 모달 닫히면 상태 초기화
  useEffect(() => {
    if (!isOpen) {
      resetForm();
    }
  }, [isOpen]);
  
  // ✅ 입력 상태 감지하는 함수
  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
    setDirty((prev) => ({ ...prev, [name]: true }));
  };

  // ✅ 선택 상자 따로 감지해야 함
  const handleSelectChange = (key, value) => {
    setForm((prev) => ({ ...prev, [key]: value }));
    setDirty((prev) => ({ ...prev, [key]: true }));
  };

  // ✅ 이미지도 따로 감지해야 함
  const handleFileChange = (file) => {
    setForm((prev) => ({ ...prev, image: file }));
  };

  // ✅ 유효성 검사
  const validate = () => {
    const newError = {};

    if (!form.name) {
      newError.name = { none: "카드 이름을 입력해 주세요." };
    } else if (form.name.length > 10) {
      newError.name = { over: "10자 이내로 입력해 주세요." };
    }

    if (!form.grade) newError.grade = "등급을 선택해 주세요.";
    if (!form.genre) newError.genre = "장르를 선택해 주세요.";

    if (!form.price) {
      newError.price = { none: "가격을 입력해 주세요." };
    } else if (!/^\\d+$/.test(form.price)) {
      newError.price = { type: "가격을 숫자로 입력해 주세요." };
    }

    if (!form.volumn) {
      newError.volumn = { none: "총 발행량을 입력해 주세요." };
    } else if (!/^\\d+$/.test(form.volumn)) {
      newError.volumn = { type: "발행량을 숫자로 입력해 주세요." };
    } else if (form.volumn > 10) {
      newError.volumn = { over: "총 발행량은 10장 이하로 선택 가능합니다." };
    }

    if (!form.image) newError.image = "파일을 선택해 주세요.";

    if (form.description.length > 60) {
      newError.description = "설명은 60자 이내로 입력해 주세요.";
    }

    const isFormValid = Object.keys(newError).length === 0;
    setErrors(newError);
    setIsValid(isFormValid);

    return isFormValid;
  };

  // ✅ 유효성 검사는 자동으로
  useEffect(() => { validate(); }, [form]);

  // ☑️ api 호출 : 모달도 같이
  const { mutate, isPending: isPostPending, isError: isPostError } = useMutation({
    mutationFn: postCard,
    onSuccess: () => {
      openModal(201, "생성", { grade: form.grade, name: form.name, count: form.volumn, });
      queryClient.invalidateQueries({ queryKey: ["creationCardCount"] });
      resetForm();
    },
    onError: (err) => {
      openModal(500, "생성", { grade: form.grade, name: form.name, count: form.volumn, });
      resetForm();
      console.error("등록 실패", err.message);
    },
  });

  // ☑️ api 호출2 - 등급/장르 가져옴
  const { data: meta, isPending: isMetaPending, isError: isMetaError } = useQuery({
    queryKey: ["cardMeta"],
    queryFn: getCardMeta,
  });
  
  // ☑️ api 호출3 - 카드 생성 횟수 가져옴
  const {
    data,
    isPending: isCountPending,
    isError: isCountError,
  } = useQuery({
    queryKey: ["creationCardCount"],
    queryFn: getMonthlyCardCount,
  });
  
  // ✅ 제출 함수
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitted(true); // 제출했어!

    const valid = validate();
    setIsValid(valid);
    if (!valid) return;

    try {
      // 여기 성훈 님이 추가했음! (upLoadImage 나중에 공부하겠음)
      const imageResponse = await upLoadImage(form.image);
      const imageUrl = imageResponse.secure_url; // 또는 imageResponse.url

      const data = { ...form, image: imageUrl };

      mutate(data); // JSON 객체로 전달
    } catch (error) {
      console.error("Cloudinary 업로드 실패:", error);
    }
  };

  return {
    form, errors, dirty, isValid, isSubmitted,
    isPending,
    handleChange, handleFileChange, handleSelectChange, handleSubmit,
  };
}