최종 프로젝트 회고 #9 - 복약 전체 리스트 내 모달 ( 수정 및 삭제)

DO YEON KIM·2024년 10월 25일
0


한 이틀 정도 못찾아온 거 같은 til. 병원 이슈와 이력서 작성 및 지원 이슈가 있었다.

오늘은 최종 프로젝트 회고의 마지막인 복용중인 약 수정 및 편집 모달에 대해 작성해보도록 하겠다.

그 전에 이 프로젝트의 노션과 깃허브 링크 등 전체적인 자료를 정리해서 남겨보고자 한다.

🟡 최종 프로젝트 버셀 배포버전

🟡 깃허브 링크

🟡 노션


🟡 폴더 구조


🟡 medi-help\src\components\templates\mypage\myPageModal\EditMediModal.tsx

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();
  • formData: 현재 수정하고 있는 약 정보의 상태를 저장. 기본 값으로 mediRecord의 내용을 복사하여 사용하고, day_of_week과 notification_time은 없을 때 빈 배열로 설정.
  • 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를 초기화

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

0개의 댓글