최종 프로젝트 회고 #5 - MediList ( 마이페이지 내 본인이 복약중인 약 목록)

DO YEON KIM·2024년 10월 17일
0


오늘은 마이페이지 창에서 현재 복약 중인 약을 보여주는 medilist에 대해 작성해보고자 한다.
마이페이지 웹 사이즈에선 최대 3개의 약물을, 모바일 사이즈에선 최대 4개의 약물을 보여주고 더보기 버튼을 클릭 시 복약 전체 리스트를 보여주는 새 창으로 이동하게 된다.

이해를 돕기 위해 버셀 배포 버전을 첨부해두겠다.

마이페이지


폴더 구조


🟡 medi-help\src\components\templates\mypage\MediLists.tsx


"use client";

import React, { useEffect, useState } from "react";
import axios from "axios";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { supabase } from "@/utils/supabase/client";
import MediModal from "./myPageModal/MediModal";

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 MediListsProps {
  className?: string;
}

const MediLists: React.FC<MediListsProps> = ({ className }) => {
  const [mediRecords, setMediRecords] = useState<MediRecord[]>([]);
  const [selectedMediRecord, setSelectedMediRecord] = useState<MediRecord | null>(null);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [displayedMediRecords, setDisplayedMediRecords] = useState<MediRecord[]>([]);
  const router = useRouter();

  useEffect(() => {
    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);
      }
    };

    fetchMediRecords();
  }, []);

  useEffect(() => {
    const handleResize = () => {
      const isMobile = window.innerWidth < 768;
      setDisplayedMediRecords(mediRecords.slice(0, isMobile ? 4 : 3));
    };

    handleResize();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [mediRecords]);

  const handleShowAllClick = () => {
    router.push("/mypage/Medications");
  };

  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' }).replace(/\. /g, '.');
  };

  return (
    <div className={`${className} w-full flex justify-center desktop:block`}>
      <div className="w-[335px] desktop:w-[670px] desktop:h-[352px] overflow-hidden  desktop:rounded-2xl desktop:bg-brand-gray-50 desktop:shadow-sm">
        <div className="desktop:px-[49px] desktop:pt-[41px] desktop:pb-[50px] h-full">
          <h2
            className="text-[16px] font-bold text-brand-gray-1000 text-left cursor-pointer mb-2 flex items-center"
            onClick={handleShowAllClick}
          >
            <span className="mb-3 text-[16px]">나의 복용약</span>
            <span className="text-[#279ef9] ml-1 mb-3 text-[16px]">
              {mediRecords.length}</span>
            <span className="text-[#279ef9] ml-1 mb-3 text-[16px]">
              &gt;
            </span>
          </h2>

          <div className="w-full h-full">
            <div className="grid grid-cols-2 gap-[17px] desktop:grid-cols-3 desktop:gap-4">
              {displayedMediRecords.map((record) => (
                <div 
                  key={record.id} 
                  className="w-[159px] desktop:w-auto"
                >
                  <div className="bg-white border border-brand-gray-50 rounded-xl flex flex-col items-center w-[159px] h-[200px] desktop:w-[180px] desktop:h-[217px] p-4">
                    <div className="w-[127px] h-[72px] desktop:w-[148px] desktop:h-[84px] 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>
                        <p className="text-[14px] desktop:text-sm font-bold text-brand-gray-1000 line-clamp-1">
                          {record.medi_nickname}
                        </p>
                        <p className="text-[12px] desktop:text-xs text-brand-gray-800 line-clamp-1 mt-1">
                          {record.medi_name}
                        </p>
                      </div>
                      <p className="text-[10px] desktop: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>
  );
};

export default MediLists;

"use client";

import React, { useEffect, useState } from "react";
import axios from "axios";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { supabase } from "@/utils/supabase/client";
import MediModal from "./myPageModal/MediModal";

클라이언트에서만 실행 될 코드이기 때문에 useclient를 선언

axios: HTTP 요청을 보낼 때 사용하는 라이브러리로, 여기서는 약 데이터를 서버에서 가져오기 위해 사용

Image: Next.js에서 제공하는 이미지 최적화 컴포넌트. 이미지를 불러올 때 최적화된 상태로 렌더링

useRouter: Next.js의 라우팅 기능을 사용하기 위한 훅. 페이지 이동 시 push 메서드를 사용


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 MediListsProps {
  className?: string;
}

약 정보를 나타내는 데이터 구조.

MediListsProps 인터페이스: MediLists 컴포넌트에 전달되는 className이라는 선택적 prop을 정의. -> 컴포넌트가 외부에서 전달된 데이터를 활용할 수 있게 하기 위함.

이는 부모 컴포넌트에서 전달받은 클래스명을 컴포넌트의 최상단 div에 추가해 주기 위해 작성.

  • className prop을 통해 부모 컴포넌트가 MediLists 컴포넌트의 스타일을 동적으로 제어 가능.

const MediLists: React.FC<MediListsProps> = ({ className }) => {

약 목록을 보여주는 컴포넌트. className prop를 받아서 컴포넌트에 추가.

React.FC (Function Component) 타입으로 선언된 함수형 컴포넌트

MediListsProps 타입을 사용하여 props로 받은 값이 className임을 나타냄


const [mediRecords, setMediRecords] = useState<MediRecord[]>([]);
const [selectedMediRecord, setSelectedMediRecord] = useState<MediRecord | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [displayedMediRecords, setDisplayedMediRecords] = useState<MediRecord[]>([]);
const router = useRouter();

상태 변수 선언.

mediRecords: 서버에서 가져온 모든 약 목록 데이터를 저장하는 상태

selectedMediRecord: 선택된 약 정보.

모달을 열 때 이 상태를 사용하여 상세 정보를 보여줌.

isModalOpen: 모달이 열렸는지 여부를 관리하는 상태.

displayedMediRecords: 화면에 보여줄 약 목록을 저장하는 상태.

router: Next.js에서 페이지 이동을 처리하기 위해 사용하는 객체. 전체 약 목록 페이지로 이동할 때 사용.


useEffect(() => {
  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);
    }
  };

  fetchMediRecords();
}, []);

약 목록 가져오기.

useEffect: 컴포넌트가 처음 렌더링될 때(마운트) 두 번째 인자인 빈 배열 덕분에 한 번만 실행

  • useEffect 훅은 React의 함수형 컴포넌트에서 사이드 이펙트를 관리하기 위해 사용.

  • 사이드 이펙트란 ?

-데이터 fetching, 구독, 타이머 설정, 수동 DOM 조작 등 컴포넌트의 렌더링과 직접적인 관련이 없는 작업을 의미

  • 일반적 구조
useEffect(() => {
  // 사이드 이펙트 로직
  return () => {
    // 정리(cleanup) 로직 (옵션)
  };
}, [의존성 배열]);

async 키워드가 사용되어 비동기 작업을 처리

supabase.auth.getSession(): Supabase의 인증 API를 사용하여 현재 사용자 세션 가져오기.

세션이 없다면 에러 메시지를 출력하고 실행 중단.

axios.get(...): 사용자의 약 목록을 서버로부터 가져오기 위해 /api/mypage/medi/names?user_id=${userId}로 GET 요청을 보내고 ( user_id 파라미터를 추가하여 특정 사용자에 대한 약 정보를 서버에 요청), 성공하면 setMediRecords를 통해 상태를 업데이트


useEffect(() => {
  const handleResize = () => {
    const isMobile = window.innerWidth < 768;
    setDisplayedMediRecords(mediRecords.slice(0, isMobile ? 4 : 3));
  };

  handleResize();
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, [mediRecords]);

화면 크기에 따라 약 목록 개수 조정

useEffect: 약 목록(mediRecords)이 변경될 때마다 실행.

첫 렌더링 시와 화면 크기가 변경될 때 handleResize가 호출되어 displayedMediRecords가 업데이트.


const handleShowAllClick = () => {
  router.push("/mypage/Medications");
};

복용 중인 약 전체 보기


const formatDate = (dateString: string) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' }).replace(/\. /g, '.');
};

날자 문자열을 받아와서 yymmdd 형식으로 변환.


return (
  <div className={`${className} w-full flex justify-center desktop:block`}>
    <div className="w-[335px] desktop:w-[670px] desktop:h-[352px] overflow-hidden desktop:rounded-2xl desktop:bg-brand-gray-50 desktop:shadow-sm">
      <div className="desktop:px-[49px] desktop:pt-[41px] desktop:pb-[50px] h-full">
        <h2
          className="text-[16px] font-bold text-brand-gray-1000 text-left cursor-pointer mb-2 flex items-center"
          onClick={handleShowAllClick}
        >
          <span className="mb-3 text-[16px]">나의 복용약</span>
          <span className="text-[#279ef9] ml-1 mb-3 text-[16px]">
            {mediRecords.length}</span>
          <span className="text-[#279ef9] ml-1 mb-3 text-[16px]">
            &gt;
          </span>
        </h2>

        <div className="w-full h-full">
          <div className="grid grid-cols-2 gap-[17px] desktop:grid-cols-3 desktop:gap-4">
            {displayedMediRecords.map((record) => (
              <div key={record.id} className="w-[159px] desktop:w-auto">
                <div className="bg-white border border-brand-gray-50 rounded-xl flex flex-col items-center w-[159px] h-[200px] desktop:w-[180px] desktop:h-[217px] p-4">
                  <div className="w-[127px] h-[72px] desktop:w-[148px] desktop:h-[84px] 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>
                      <p className="text-[14px] desktop:text-sm font-bold text-brand-gray-1000 line-clamp-1">
                        {record.medi_nickname}
                      </p>
                      <p className="text-[12px] desktop:text-xs text-brand-gray-800 line-clamp-1 mt-1">
                        {record.medi_name}
                      </p>
                    </div>
                    <p className="text-[10px] desktop: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>
);

반응형 레이아웃이 들어간 리턴 코드.

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

0개의 댓글