오늘은 이전에 언급했던 전체 복약 기록에 대해 작성해보고자 한다.
마이페이지에선 캘린더 페이지에서 등록한 복용중인 약을 UI상 일정 개수만큼만 보여주는데, 아래와 같이 전체 복약 목록을 확인할 수 있는 화살표 버튼을 누르면
전체 복약 목록을 확인하는 새 창으로 이동할 수 있다.
폴더 구조는 이전과 같으니 생략하겠다.
"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}
>
<
</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}
>
>
</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);
}
};
화면 크기에 따른 페이지네이션 기능.
그 외 리턴 코드는 테일윈드여서 패스하겠다.
남은 회고 작성 컨텐츠는 캘린더 페이지 내 모달과 사이드바에 있는 기능, 모달창들, 그리고 리팩토링 예정이다.