yeliinbb / Ai-todo-app

AI 챗봇과 함께하는 통합 일정 관리 서비스
https://ai-todo-app-beta.vercel.app
1 stars 1 forks source link

[useModal] 공통 모달 커스텀 훅 #159

Open yeliinbb opened 1 month ago

yeliinbb commented 1 month ago

useModal.tsx

"use client";
import CloseBtn from "@/components/icons/modal/CloseBtn";
import ModalBtn from "@/components/modal/ModalBtn";
import React, { useCallback, useEffect, useState } from "react";
import ReactModal from "react-modal";

type ButtonConfig = {
  text: string;
  style: string;
};

type ModalConfig = {
  message: string;
  confirmButton: ButtonConfig;
  cancelButton?: ButtonConfig;
};

const useModal = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [config, setConfig] = useState<ModalConfig>({
    message: "",
    confirmButton: { text: "", style: "" },
    cancelButton: undefined
  });
  const [onConfirm, setOnConfirm] = useState<(() => void) | null>(null);

  const openModal = useCallback((newConfig: ModalConfig, confirmCallback?: () => void) => {
    setConfig(newConfig);
    setIsModalOpen(true);
    setOnConfirm(() => confirmCallback || null);
  }, []);

  const closeModal = useCallback(() => {
    setIsModalOpen(false);
    setOnConfirm(null);
  }, []);

  const handleConfirm = useCallback(() => {
    if (onConfirm) {
      onConfirm();
    }
    closeModal();
  }, [onConfirm, closeModal]);

  const handleCancel = useCallback(() => {
    closeModal();
  }, [closeModal]);

  const getButtonStyle = (style: string) => {
    switch (style) {
      case "확인":
        return "bg-system-red200 text-system-white";
      case "취소":
        return "bg-system-white border border-solid border-gray-400 text-system-black";
      case "삭제":
        return "bg-system-red200 text-system-white";
      case "시스템":
        return "bg-gradient-pai400-fai500-br text-system-white hover:border-paiTrans-60032 active:bg-gradient-pai600-fai700-br";
      default:
        return style;
    }
  };

  const Modal = () => {
    return (
      <ReactModal
        isOpen={isModalOpen}
        onRequestClose={handleCancel}
        className="text-center bg-whiteTrans-wh72 mobile:w-[calc(100%-32px)] mx-auto rounded-[32px] p-6 desktop:w-[343px] outline-none"
        overlayClassName="fixed inset-0 bg-modalBg-black40 backdrop-blur-md z-[10000] flex items-center justify-center"
        ariaHideApp={false}
        shouldCloseOnEsc={true}
        shouldCloseOnOverlayClick={true}
      >
        <div className="mb-5 relative">
          <CloseBtn btnStyle={"absolute right-0 top-[-6px] cursor-pointer"} onClick={handleCancel} />
          <div className="flex flex-col min-h-16 items-center justify-center">
            {config.message.split("\n").map((line, index) => (
              <React.Fragment key={index}>
                <span className="flex items-center justify-center font-medium text-gray-900 text-base leading-[27px]">
                  {line}
                </span>
              </React.Fragment>
            ))}
          </div>
        </div>
        <div className="flex justify-center gap-2">
          {config.cancelButton && (
            <ModalBtn
              className={getButtonStyle(config.cancelButton.style)}
              onClick={handleCancel}
              text={config.cancelButton.text}
            />
          )}
          <ModalBtn
            className={getButtonStyle(config.confirmButton.style)}
            onClick={handleConfirm}
            text={config.confirmButton.text}
          />
        </div>
      </ReactModal>
    );
  };

  return {
    isModalOpen,
    openModal,
    closeModal,
    Modal
  };
};

export default useModal;

컴포넌트 내에서 사용 방법

 const { openModal, Modal } = useModal();

openModal(
          // 첫번째 인자는 모달창 스타일과 관련된 내용
        {
          message: /* 모달창에 들어갈 텍스트 string으로 넣기 */,
          confirmButton: { text: /* 버튼에 들어갈 텍스트 string으로 넣기*/, style: /* useModal 훅에서 getButtonStyle 함수 switch문 case에 있는 텍스트 확인 후 맞는 케이스 넣기*/}
          // cancelButton 필요 시 넣기 (현재는 회원탈퇴 페이지에서만 쓰임)
        },
        // 확인 버튼 클릭 시 실행될 함수 두번째 인자로 넣기
        () => {
          router.push("/login");
        }
      );

사용 예시 1 (취소 버튼 없는 경우)

"use client";
import { AIType } from "@/types/chat.session.type";
import SessionBtn from "./_components/SessionBtn";
import { Metadata } from "next";
import useModal from "@/hooks/useModal";
import { useRouter } from "next/navigation";

const metadata: Metadata = {
  title: "PAi 채팅 페이지",
  description: "PAi/FAi 채팅 페이지입니다.",
  keywords: ["chat", "assistant", "friend"],
  openGraph: {
    title: "채팅 페이지",
    description: "PAi/FAi 채팅 페이지입니다.",
    type: "website"
  }
};

const aiTypes: AIType[] = ["assistant", "friend"];

const ChatPage = () => {
  // TODO : 여기서 리스트 불러오면 prefetch 사용해서 렌더링 줄이기
  const { openModal, Modal } = useModal();
  const router = useRouter();

  const handleUnauthorized = () => {
    openModal(
      {
        message: "로그인 이후 사용가능한 서비스입니다.\n로그인페이지로 이동하시겠습니까?",
        confirmButton: { text: "확인", style: "시스템" }
      },
      () => router.push("/login")
    );
  };

  return (
    <>
// 모달창 띄우는 컴포넌트에 모달 컴포넌트 넣기
// 하단이든 상단이는 위치는 상관없지만 상단에 두었을 때 모달이 쓰인다는 것을 직관적으로 알 수 있어 상단에 위치시키는 것을 더 권장
      <Modal />
      <div className="gradient-container w-full h-full rounded-t-[60px]">
        <div className="gradient-rotated gradient-ellipse w-full h-[90%]"></div>
        <div className="relative z-10 w-full h-full">
          <div className="flex flex-col items-center justify-center w-full h-full">
            <span className="text-gray-600 font-medium text-lg">어떤 파이와 이야기해 볼까요?</span>
            <div className="flex flex-col p-4 gap-6">
              {aiTypes.map((aiType) => (
                <SessionBtn key={aiType} aiType={aiType} handleUnauthorized={handleUnauthorized} />
              ))}
            </div>
          </div>
        </div>
      </div>
    </>
  );
};

export default ChatPage;

사용 예시 2 (취소 버튼 있는 경우)

  const handleClickDelete = () => {
    if (!isAgreement) {
      toast.warn("회원 탈퇴 유의사항에 동의해주세요.");
      return;
    }
    openModal(
      {
        message: "정말 탈퇴하시겠어요?",
        confirmButton: { text: "확인", style: "확인" },
        cancelButton: { text: "취소", style: "취소" }
      },
      handleDeleteAccount
    );
  };
oneieo commented 1 month ago

사용 예시까지 깔끔하게 알려주셔서 감사해요😊 바로 적용해보겠습니다!! 🫡