최종 프로젝트 회고 #6 - Medications (마이페이지 내 전체 복약 기록 확인 및 수정)

DO YEON KIM·2024년 10월 18일
0


오늘은 이전에 언급했던 전체 복약 기록에 대해 작성해보고자 한다.

마이페이지에선 캘린더 페이지에서 등록한 복용중인 약을 UI상 일정 개수만큼만 보여주는데, 아래와 같이 전체 복약 목록을 확인할 수 있는 화살표 버튼을 누르면

전체 복약 목록을 확인하는 새 창으로 이동할 수 있다.


폴더 구조는 이전과 같으니 생략하겠다.


🟡 src\components\templates\mypage\Medications.tsx

"use client";

import React, { useEffect, useState } from "react";
import axios from "axios";
import { supabase } from "@/utils/supabase/client";
import Image from "next/image";
import EditMediModal from "./myPageModal/EditMediModal";
import MediModal from "./myPageModal/MediModal";
import MyPageViewModal from '@/components/molecules/MyPageViewModal';
import { format } from "date-fns";
import { useToast } from "@/hooks/useToast";


interface MediRecord {
  id: string;
  medi_name: string;
  medi_nickname: string;
  times: {
    morning: boolean;
    afternoon: boolean;
    evening: boolean;
  };
  notes: string;
  start_date: string;
  end_date: string;
  created_at: string;
  itemImage?: string | null;
  user_id: string;
  notification_time?: string[];
  day_of_week?: string[];
  repeat?: boolean;
}

const formatDate = (dateString: string) => {
  const date = new Date(dateString);
  return format(date, "yy.MM.dd");
};

const Medications: React.FC = () => {
  const [mediRecords, setMediRecords] = useState<MediRecord[]>([]);
  const [selectedMediRecord, setSelectedMediRecord] = useState<MediRecord | null>(null);
  const [isViewModalOpen, setIsViewModalOpen] = useState(false);
  const [isEditModalOpen, setIsEditModalOpen] = useState(false);
  const [currentPage, setCurrentPage] = useState(1);
  const [isMobile, setIsMobile] = useState(false);
  const [isMobileViewOpen, setIsMobileViewOpen] = useState(false);
  const { toast } = useToast()

  const fetchMediRecords = async () => {
    const session = await supabase.auth.getSession();
    if (!session.data.session) {
      console.error("Auth session missing!");
      return;
    }
    const userId = session.data.session.user.id;
    try {
      const response = await axios.get(`/api/mypage/medi/names?user_id=${userId}`);
      setMediRecords(response.data);
    } catch (error) {
      console.error("Error fetching medi records:", error);
    }
  };

  useEffect(() => {
    fetchMediRecords();
    const handleResize = () => {
      setIsMobile(window.innerWidth < 768);
    };
    handleResize();
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  const handleMediClick = (record: MediRecord) => {
    setSelectedMediRecord(record);
    if (isMobile) {
      setIsMobileViewOpen(true);
    } else {
      setIsViewModalOpen(true);
    }
  };

  const closeAllModals = () => {
    setIsViewModalOpen(false);
    setIsEditModalOpen(false);
    setIsMobileViewOpen(false);
    setSelectedMediRecord(null);
  };

  const handleUpdate = async (updatedMediRecord: MediRecord) => {
    try {
      await axios.put(`/api/mypage/medi/${updatedMediRecord.id}`, updatedMediRecord);
      await fetchMediRecords();
      closeAllModals();
      setTimeout(() => {
        toast.success("약 정보가 성공적으로 수정되었습니다.");
      }, 300); // 모달이 완전히 닫힌 후 토스트 표시
    } catch (error) {
      console.error("Error updating medication:", error);
      toast.error("약 정보 수정 중 오류가 발생했습니다.");
    }
  };


  const handleDelete = async (id: string) => {
    try {
      await axios.delete(`/api/mypage/medi/${id}`);
      await fetchMediRecords();
      closeAllModals();
      setTimeout(() => {
        toast.success("약 정보가 삭제되었습니다.");
      }, 300); // 모달이 완전히 닫힌 후 토스트 표시
    } catch (error) {
      console.error("Error deleting medication:", error);
      toast.error("약 정보 삭제 중 오류가 발생했습니다.");
    }
  };
  const openEditModal = () => {
    setIsViewModalOpen(false);
    setIsEditModalOpen(true);
  };

  const ITEMS_PER_PAGE = isMobile ? 8 : 15;
  const totalPages = Math.ceil(mediRecords.length / ITEMS_PER_PAGE);
  const currentRecords = mediRecords.slice(
    (currentPage - 1) * ITEMS_PER_PAGE,
    currentPage * ITEMS_PER_PAGE
  );

  const handlePageChange = (page: number) => {
    if (page > 0 && page <= totalPages) {
      setCurrentPage(page);
    }
  };

  return (
    <div className="max-w-screen-xl mx-auto px-4 py-4 overflow-x-hidden">
      <div className="w-full md:w-[670px] mx-auto mt-16 md:mt-24">
        <div className="relative">
          <div className="relative">
            <h2 className={`text-[20px] md:text-[24px] font-bold text-brand-gray-800 mb-4 ${isMobile ? 'absolute left-0 top-0 w-full px-4' : 'mb-8'}`}>
              복약 리스트
            </h2>
            
            <div className={`flex justify-center ${isMobile ? 'pt-16' : ''}`}>
              <div className={`overflow-hidden ${isMobile ? 'w-[335px]' : 'w-full'}`}>
                <div
                  className={`grid ${
                    isMobile ? "grid-cols-2" : "grid-cols-5"
                  } gap-${isMobile ? '4' : '6'}`}
                  style={{ gap: isMobile ? '17px' : '24px' }}
                >
                  {currentRecords.map((record) => (
                    <div
                      key={record.id}
                      className={`bg-white border border-brand-gray-50 rounded-xl flex flex-col items-center cursor-pointer p-4 ${
                        isMobile ? "w-[159px] h-[200px]" : "w-[159px] h-[200px]"
                      }`}
                      onClick={() => handleMediClick(record)}
                    >
                      <div className="w-[127px] h-[72px] mb-2">
                        {record.itemImage ? (
                          <Image
                            src={record.itemImage}
                            alt={record.medi_nickname || "약 이미지"}
                            width={127}
                            height={72}
                            layout="responsive"
                            objectFit="cover"
                            className="rounded-lg"
                          />
                        ) : (
                          <div className="w-full h-full flex items-center justify-center bg-brand-gray-200 rounded-lg">
                            <p className="text-brand-gray-400 text-xs">이미지 없음</p>
                          </div>
                        )}
                      </div>
                      <div className="flex flex-col justify-between w-full flex-grow">
                        <div className="mb-2">
                          <p className="text-[14px] md:text-sm font-bold text-brand-gray-1000 line-clamp-1">
                            {record.medi_nickname}
                          </p>
                          <p className="text-[12px] md:text-xs text-brand-gray-800 line-clamp-1 mt-1">
                            {record.medi_name}
                          </p>
                        </div>
                        <p className="text-[10px] md:text-xs text-brand-primary-500 truncate mt-4">
                          {formatDate(record.start_date)} ~ {formatDate(record.end_date)}
                        </p>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
        
        <div className="flex justify-center mt-4 space-x-1">
          <button
            onClick={() => handlePageChange(currentPage - 1)}
            className={`px-4 py-2 ${
              currentPage === 1
                ? "text-brand-gray-400 cursor-not-allowed"
                : "text-brand-gray-700"
            }`}
            disabled={currentPage === 1}
          >
            &lt;
          </button>
          {Array.from({ length: totalPages }, (_, index) => (
            <button
              key={index}
              onClick={() => handlePageChange(index + 1)}
              className={`px-4 py-2 ${
                currentPage === index + 1
                  ? "text-brand-primary-600"
                  : "text-brand-gray-700"
              }`}
            >
              {index + 1}
            </button>
          ))}
          <button
            onClick={() => handlePageChange(currentPage + 1)}
            className={`px-4 py-2 ${
              currentPage === totalPages
                ? "text-brand-gray-400 cursor-not-allowed"
                : "text-brand-gray-700"
            }`}
            disabled={currentPage === totalPages}
          >
            &gt;
          </button>
        </div>
      </div>

      {selectedMediRecord && (
        <>
          {isMobile ? (
            <MyPageViewModal
              isOpen={isMobileViewOpen}
              onClose={closeAllModals}
              mediRecord={selectedMediRecord}
              onUpdate={handleUpdate}
              onDelete={handleDelete}
            />
          ) : (
            <>
              <MediModal
                isOpen={isViewModalOpen}
                onRequestClose={() => setIsViewModalOpen(false)}
                onEditClick={openEditModal}
                mediRecord={selectedMediRecord}
              />
              <EditMediModal
                isOpen={isEditModalOpen}
                onRequestClose={closeAllModals}
                onDelete={handleDelete}
                onUpdate={handleUpdate}
                mediRecord={selectedMediRecord}
              />
            </>
          )}
        </>
      )}
    </div>
  );
};

export default Medications;

interface MediRecord {
  id: string;
  medi_name: string;
  medi_nickname: string;
  times: {
    morning: boolean;
    afternoon: boolean;
    evening: boolean;
  };
  notes: string;
  start_date: string;
  end_date: string;
  created_at: string;
  itemImage?: string | null;
  user_id: string;
  notification_time?: string[];
  day_of_week?: string[];
  repeat?: boolean;
}

약 정보를 담고있는 타입.

여기서 들만한 의문. interface의 중복 .. 물론 types.ts를 만들어두고 똑같이 두긴 했지만 타입 에러가 계속 나기도 하고 잦은 수정이 있기도 해서 그대로 두고 사용하였다.


const formatDate = (dateString: string) => {
  const date = new Date(dateString);
  return format(date, "yy.MM.dd");
};

날짜를 yymmdd로 변환하는 유틸리티 함수.
db에서 가져온 날짜 문자열을 가독성 좋은 형식으로 반환한다.


const [mediRecords, setMediRecords] = useState<MediRecord[]>([]);
const [selectedMediRecord, setSelectedMediRecord] = useState<MediRecord | null>(null);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [isMobile, setIsMobile] = useState(false);
const [isMobileViewOpen, setIsMobileViewOpen] = useState(false);
const { toast } = useToast()

그리고 수많은 useState들과 toast 훅

<>는 제너릭을 사용하여 타입의 형태를 명확히 정의.

첫 번째 줄은 배열 타입을 정의하고, 두번 째 줄은 객체 또는 null을 허용하는 상태를 정의. 코드의 타입 안정성을 높일 수 있음.

  • 그럼 안쓰는 경우는 딱히 안전하지 않아도 되는 경우인가?

-> 타입이 명확하지 않거나 불필요한 경우( 특정 타입으로 명확히 설정된 경우)엔 불필요.

-> 타입 정의가 공통적인 경우.

-> props에서 이미 타입을 정의한 경우

ex)

interface User {
  name: string;
  age: number;
}

const UserProfile: React.FC<{ user: User }> = ({ user }) => {
  const [currentUser, setCurrentUser] = useState(user); // user를 기반으로 타입 추론
};

const fetchMediRecords = async () => {
  const session = await supabase.auth.getSession();
  if (!session.data.session) {
    console.error("Auth session missing!");
    return;
  }
  const userId = session.data.session.user.id;
  try {
    const response = await axios.get(`/api/mypage/medi/names?user_id=${userId}`);
    setMediRecords(response.data);
  } catch (error) {
    console.error("Error fetching medi records:", error);
  }
};

사용자의 인증 세션을 확인 후, 해당 유저의 id로 약 목록을 api에서 가져온다.

가져온 데이터는 medirecords상태에 저장.


useEffect(() => {
  fetchMediRecords();
  const handleResize = () => {
    setIsMobile(window.innerWidth < 768);
  };
  handleResize();
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

컴포넌트가 처음 렌더링 될 때 약 목록을 불러오고, 화면 크기가 변할 때 확인하여 상태를 업데이트


const handleMediClick = (record: MediRecord) => {
  setSelectedMediRecord(record);
  if (isMobile) {
    setIsMobileViewOpen(true);
  } else {
    setIsViewModalOpen(true);
  }
};

약 카드 클릭 시 해당 약의 정보를 selectedMediredirecord에 저장하고, 화면 크기에 맞는 모달 오픈


const closeAllModals = () => {
  setIsViewModalOpen(false);
  setIsEditModalOpen(false);
  setIsMobileViewOpen(false);
  setSelectedMediRecord(null);
};

모달 닫기

선택된 약의 정보도 초기화


const handleUpdate = async (updatedMediRecord: MediRecord) => {
  try {
    await axios.put(`/api/mypage/medi/${updatedMediRecord.id}`, updatedMediRecord);
    await fetchMediRecords();
    closeAllModals();
    setTimeout(() => {
      toast.success("약 정보가 성공적으로 수정되었습니다.");
    }, 300);
  } catch (error) {
    console.error("Error updating medication:", error);
    toast.error("약 정보 수정 중 오류가 발생했습니다.");
  }
};

약 정보가 수정되면 서버에 저장한 뒤, 목록을 다시 불러오고 모달을 닫기


const handleDelete = async (id: string) => {
  try {
    await axios.delete(`/api/mypage/medi/${id}`);
    await fetchMediRecords();
    closeAllModals();
    setTimeout(() => {
      toast.success("약 정보가 삭제되었습니다.");
    }, 300);
  } catch (error) {
    console.error("Error deleting medication:", error);
    toast.error("약 정보 삭제 중 오류가 발생했습니다.");
  }
};

약 정보 삭제 시 약 정보를 서버에서 삭제한 뒤, 목록을 다시 불러오고 모달 닫기


const ITEMS_PER_PAGE = isMobile ? 8 : 15;
const totalPages = Math.ceil(mediRecords.length / ITEMS_PER_PAGE);
const currentRecords = mediRecords.slice(
  (currentPage - 1) * ITEMS_PER_PAGE,
  currentPage * ITEMS_PER_PAGE
);

const handlePageChange = (page: number) => {
  if (page > 0 && page <= totalPages) {
    setCurrentPage(page);
  }
};

화면 크기에 따른 페이지네이션 기능.


그 외 리턴 코드는 테일윈드여서 패스하겠다.

남은 회고 작성 컨텐츠는 캘린더 페이지 내 모달과 사이드바에 있는 기능, 모달창들, 그리고 리팩토링 예정이다.

profile
프론트엔드 개발자를 향해서

0개의 댓글