한 이틀 정도 못찾아온 거 같은 til. 병원 이슈와 이력서 작성 및 지원 이슈가 있었다.
오늘은 최종 프로젝트 회고의 마지막인 복용중인 약 수정 및 편집 모달에 대해 작성해보도록 하겠다.
그 전에 이 프로젝트의 노션과 깃허브 링크 등 전체적인 자료를 정리해서 남겨보고자 한다.
🟡 깃허브 링크
🟡 노션
import React, { useState, useEffect } from "react";
import Modal from "react-modal";
import axios from "axios";
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;
user_id: string;
itemImage?: string | null;
notification_time?: string[];
day_of_week?: string[];
repeat?: boolean;
is_sent?: false;
}
interface EditMediModalProps {
isOpen: boolean;
onRequestClose: () => void;
onUpdate: (updatedMediRecord: MediRecord) => void;
onDelete: (id: string) => void;
mediRecord: MediRecord;
}
const EditMediModal: React.FC<EditMediModalProps> = ({
isOpen,
onRequestClose,
onUpdate,
onDelete,
mediRecord,
}) => {
const [formData, setFormData] = useState<MediRecord>({
...mediRecord,
day_of_week: mediRecord.day_of_week || [],
notification_time: mediRecord.notification_time || [],
});
const [mediNames, setMediNames] = useState<{ itemName: string }[]>([]);
const [notificationEnabled, setNotificationEnabled] = useState(!!mediRecord.repeat);
const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast();
useEffect(() => {
const toastContainer = document.querySelector('.Toastify');
if (toastContainer) {
(toastContainer as HTMLElement).style.zIndex = '10000'; // 매우 높은 z-index 값 설정
}
}, []);
const showToast = (message: string, type: 'success' | 'error') => {
toast[type](message);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
if (name === 'medi_nickname' && value.length > 6) {
showToast("약 별명은 최대 6글자입니다.", "error");
return;
}
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
const handleTimeChange = (time: 'morning' | 'afternoon' | 'evening') => {
setFormData((prevData) => ({
...prevData,
times: {
...prevData.times,
[time]: !prevData.times[time],
},
}));
};
const handleDayOfWeekChange = (day: string) => {
setFormData((prevData) => ({
...prevData,
day_of_week: prevData.day_of_week
? prevData.day_of_week.includes(day)
? prevData.day_of_week.filter((d) => d !== day)
: [...prevData.day_of_week, day]
: [day],
}));
};
const handleNotificationTimeChange = (value: string) => {
setFormData((prevData) => ({
...prevData,
notification_time: [value],
}));
};
const handleDeleteClick = async () => {
try {
await axios.delete(`/api/mypage/medi/${formData.id}`);
onDelete(formData.id);
onRequestClose();
} catch (error) {
console.error("Error deleting medication record:", error);
showToast("약 정보 삭제 중 오류가 발생했습니다.", "error");
}
};
const validateForm = () => {
if (!formData.medi_nickname.trim()) {
showToast("약 별명을 입력해주세요.", "error");
return false;
}
if (!formData.medi_name) {
showToast("약 이름을 선택해주세요.", "error");
return false;
}
if (!formData.start_date || !formData.end_date) {
showToast("복용 기간을 선택해주세요.", "error");
return false;
}
return true;
};
const handleUpdateClick = async () => {
if (!validateForm()) return;
try {
await axios.put(`/api/mypage/medi/${formData.id}`, {
...formData,
repeat: notificationEnabled,
});
onUpdate(formData);
onRequestClose();
} catch (error) {
console.error("Error updating medication record:", error);
showToast("약 정보 수정 중 오류가 발생했습니다.", "error");
}
};
const fetchMediNames = async () => {
setIsLoading(true);
try {
const response = await axios.get('/api/calendar/medi/names');
setMediNames(response.data);
} catch (error) {
console.error("Error fetching medication names:", error);
toast.error("약 이름 목록을 불러오는 데 실패했습니다.");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchMediNames();
}, []);
useEffect(() => {
setFormData({
...mediRecord,
day_of_week: mediRecord.day_of_week || [],
notification_time: mediRecord.notification_time || [],
});
setNotificationEnabled(!!mediRecord.repeat);
}, [mediRecord]);
return (
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
contentLabel="Edit Medication"
className="fixed inset-0 flex items-center justify-center z-50"
overlayClassName="fixed inset-0 bg-gray-900 bg-opacity-75 z-40"
ariaHideApp={false}
>
<div className="bg-white rounded-lg p-6 max-w-[432px] w-full max-h-[90vh] overflow-y-auto relative">
<button
onClick={onRequestClose}
className="absolute top-6 right-6 text-gray-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<h2 className="text-[16px] font-bold mb-5 text-brand-gray-800">나의 약</h2>
<div className="mb-2">
<input
type="text"
name="medi_nickname"
placeholder="약 별명(최대 6자)"
value={formData.medi_nickname}
onChange={handleInputChange}
className="border rounded w-full h-[40px] py-2 px-3 text-brand-gray-1000 leading-tight focus:outline-none"
/>
</div>
<div className="mb-5">
<select
name="medi_name"
value={formData.medi_name}
onChange={handleInputChange}
className="border rounded w-full h-[40px] py-2 px-3 text-brand-gray-1000 leading-tight focus:outline-none"
disabled={isLoading}
>
<option value="">약 이름 선택</option>
{isLoading ? (
<option value="" disabled>로딩 중...</option>
) : (
mediNames.map((item, index) => (
<option key={index} value={item.itemName}>
{item.itemName}
</option>
))
)}
</select>
</div>
<div className="mb-5">
<label className="block text-[14px] font-bold mb-2 text-brand-gray-600">나의 약 등록:</label>
<div className="flex space-x-4 text-brand-gray-800 justify-between w-full">
{['morning', 'afternoon', 'evening'].map((time) => (
<button
key={time}
type="button"
onClick={() => handleTimeChange(time as 'morning' | 'afternoon' | 'evening')}
className={`px-4 py-2 rounded-full ${
formData.times[time as 'morning' | 'afternoon' | 'evening']
? "bg-brand-primary-500 text-white"
: "bg-brand-gray-50 text-brand-gray-800"
} w-1/3`}
>
{time === 'morning' ? '아침' : time === 'afternoon' ? '점심' : '저녁'}
</button>
))}
</div>
</div>
<div className="flex space-x-4 mb-4">
<div className="w-1/2 flex items-center">
<div className="flex items-center">
<input
type="date"
name="start_date"
value={formData.start_date}
onChange={handleInputChange}
className="border rounded py-2 px-3 text-brand-gray-800 leading-tight focus:outline-none w-3/4"
/>
<span className="ml-3 text-brand-gray-800">부터</span>
</div>
</div>
<div className="w-1/2 flex items-center">
<div className="flex items-center">
<input
type="date"
name="end_date"
value={formData.end_date}
onChange={handleInputChange}
className="border rounded py-2 px-3 text-brand-gray-800 leading-tight focus:outline-none w-3/4"
/>
<span className="ml-3 text-brand-gray-800">까지</span>
</div>
</div>
</div>
<div className="flex items-center mb-4">
<label className="flex items-center">
<span className="ml-2 text-brand-gray-600">알림 설정 </span>
<div
onClick={() => setNotificationEnabled(!notificationEnabled)}
className={`relative w-12 h-6 flex items-center rounded-full ml-3 cursor-pointer ${
notificationEnabled ? "bg-brand-primary-400" : "bg-brand-gray-400"
}`}
>
<div
className={`absolute w-6 h-6 bg-white rounded-full transition-transform transform ${
notificationEnabled ? "translate-x-6" : "translate-x-0"
}`}
></div>
</div>
</label>
</div>
{notificationEnabled && (
<>
<div className="mb-4">
<div className="flex justify-between w-full mb-4">
{["월", "화", "수", "목", "금", "토", "일"].map((day) => (
<button
key={day}
type="button"
onClick={() => handleDayOfWeekChange(day)}
className={`w-[36px] h-[36px] rounded-full flex items-center justify-center text-[16px] font-bold ${
formData.day_of_week?.includes(day)
? "bg-blue-500 text-white"
: "bg-gray-200 text-brand-gray-800"
}`}
>
{day}
</button>
))}
</div>
</div>
<div className="mb-5">
<input
type="time"
name="notification_time"
value={formData.notification_time?.[0] || ""}
onChange={(e) => handleNotificationTimeChange(e.target.value)}
className="border rounded w-full h-[40px] py-2 px-3 text-gray-700 leading-tight focus:outline-none"
/>
</div>
</>
)}
<div className="mb-10">
<label className="block text-[16px] font-bold mb-2 text-brand-gray-600">메모</label>
<textarea
name="notes"
value={formData.notes}
onChange={handleInputChange}
placeholder="약에 대한 간단한 기록"
className="border rounded w-full h-[80px] py-2 px-3 text-gray-700 leading-tight focus:outline-none resize-none"
/>
</div>
<div className="flex justify-center space-x-4 mt-4">
<button
type="button"
onClick={handleDeleteClick}
className="px-4 py-2 rounded-md bg-brand-primary-50 text-brand-primary-500"
>
삭제
</button>
<button
type="button"
onClick={handleUpdateClick}
className="px-4 py-2 rounded-md bg-brand-primary-500 text-brand-primary-50"
>
수정
</button>
</div>
</div>
</Modal>
);
};
export default EditMediModal;
interface EditMediModalProps {
isOpen: boolean;
onRequestClose: () => void;
onUpdate: (updatedMediRecord: MediRecord) => void;
onDelete: (id: string) => void;
mediRecord: MediRecord;
}
onUpdate: 업데이트된 약 정보 MediRecord를 부모 컴포넌트에 전달하는 함수
onDelete: 약 정보 삭제 시 호출되는 함수로, 삭제할 약의 id를 인자로 받음.
mediRecord: 현재 편집 중인 약 정보를 나타냄.
const EditMediModal: React.FC<EditMediModalProps> = ({ isOpen, onRequestClose, onUpdate, onDelete, mediRecord }) => {
const [formData, setFormData] = useState<MediRecord>({
...mediRecord,
day_of_week: mediRecord.day_of_week || [],
notification_time: mediRecord.notification_time || [],
});
const [mediNames, setMediNames] = useState<{ itemName: string }[]>([]);
const [notificationEnabled, setNotificationEnabled] = useState(!!mediRecord.repeat);
const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast();
mediNames: 약 이름을 받아와 저장할 배열 형태의 상태. itemName 속성을 가진 객체의 배열로 설정.
notificationEnabled: 약 복용 알림 기능을 사용할지 여부. mediRecord의 repeat 값이 true이면 notificationEnabled도 true.
isLoading: 약 이름 목록을 서버에서 불러오는 동안 로딩 상태를 나타내는 Boolean 값.
useEffect(() => {
const toastContainer = document.querySelector('.Toastify');
if (toastContainer) {
(toastContainer as HTMLElement).style.zIndex = '10000';
}
}, []);
그리고 토스트 알림이 자꾸 모달 뒤 회색 배경 뒤에서 떠서 넣어본 코드인데, 여전히 뒤에서 뜬다. (= 회색 배경과 어울어져 잘 안보이게 뜬다.)
튜터님한테 여쭤봤는데, z-index 값을 줘도 이러는거면 라이브러리 세팅 자체가 이렇게 되어있어 어쩔 수 없다고 하셨다.
const showToast = (message: string, type: 'success' | 'error') => {
toast[type](message);
};
type에 따라 성공 또는 실패를 나타냄.
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
if (name === 'medi_nickname' && value.length > 6) {
showToast("약 별명은 최대 6글자입니다.", "error");
return;
}
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
입력 필드 변경 함수.
입력 이벤트에 반응하여 입력된 데이터를 formData 상태에 업데이트
input, textarea, 또는 select 요소에서 입력이 변경될 때 호출.
name과 value는 이벤트가 발생한 요소의 name 속성과 value 값을 가져오되, 유효성 검사 후 진행. setFormData를 호출하여 formData 상태를 업데이트. prevData는 이전 상태이며, 변경된 name 속성에 맞춰 value 값이 갱신.
const handleTimeChange = (time: 'morning' | 'afternoon' | 'evening') => {
setFormData((prevData) => ({
...prevData,
times: {
...prevData.times,
[time]: !prevData.times[time],
},
}));
};
특정 시간대의 상태를 반전시켜 시간대 설정 or 해제 가능.
!prevData.times[time] 부분은 기존 상태의 true를 false로, false를 true로 반전시키는 부분
const handleDayOfWeekChange = (day: string) => {
setFormData((prevData) => ({
...prevData,
day_of_week: prevData.day_of_week
? prevData.day_of_week.includes(day)
? prevData.day_of_week.filter((d) => d !== day) //이미 있는 경우 제거
: [...prevData.day_of_week, day]
: [day],
}));
};
요일 선택 함수 정의
선택된 요일을 상태에 추가하거나, 이미 포함 되어 있는 경우 제거하여 반복 요일을 관리.
현재 요일 배열에 클릭한 day가 포함되어 있다면, 그 day는 filter를 이용해 제거
포함되어 있지 않으면, day_of_week 배열 끝에 해당 day를 추가
const handleNotificationTimeChange = (value: string) => {
setFormData((prevData) => ({
...prevData,
notification_time: [value],
}));
};
알림 시간을 설정하여 상태에 저장.
const handleDeleteClick = async () => {
try {
await axios.delete(`/api/mypage/medi/${formData.id}`);
onDelete(formData.id);
onRequestClose();
} catch (error) {
console.error("Error deleting medication record:", error);
showToast("약 정보 삭제 중 오류가 발생했습니다.", "error");
}
};
삭제 함수.
axios를 통해 약 정보를 삭제하고, 삭제가 성공하면 onDelete 및 onRequestClose를 호출
nDelete(formData.id)를 호출해 부모 컴포넌트에 삭제된 정보를 전달하고, onRequestClose()를 호출해 모달 창을 닫기.
오류가 발생할 경우 오류 메시지 출력.
const handleUpdateClick = async () => {
if (!validateForm()) return;
try {
await axios.put(`/api/mypage/medi/${formData.id}`, {
...formData,
repeat: notificationEnabled,
});
onUpdate(formData);
onRequestClose();
} catch (error) {
console.error("Error updating medication record:", error);
showToast("약 정보 수정 중 오류가 발생했습니다.", "error");
}
};
업데이트 함수.
axios를 통해 서버에 업데이트 요청.
성공 시 onUpdate와 onRequestClose를 호출하고, 오류 시 메시지를 표시
const fetchMediNames = async () => {
setIsLoading(true);
try {
const response = await axios.get('/api/calendar/medi/names');
setMediNames(response.data);
} catch (error) {
console.error("Error fetching medication names:", error);
toast.error("약 이름 목록을 불러오는 데 실패했습니다.");
} finally {
setIsLoading(false);
}
};
axios를 통해 약 이름 목록을 서버에서 받아와 mediNames 상태에 저장.
useEffect(() => {
fetchMediNames();
}, []);
useEffect(() => {
setFormData({
...mediRecord,
day_of_week: mediRecord.day_of_week || [],
notification_time: mediRecord.notification_time || [],
});
setNotificationEnabled(!!mediRecord.repeat);
}, [mediRecord]);
컴포넌트가 렌더링될 때 , fetchMediNames함수를 호출하여 약 이름 목록을 불러오기
mediRecord가 변경될 때마다 formData와 notificationEnabled를 초기화