Skip to content

Latest commit

 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🍅 싱싱고: 식료품 관리 웹 애플리케이션

🎯 프로젝트 개요

리액트를 활용하여 식료품 관리 시스템을 구현하며, 팀워크를 바탕으로 프로젝트를 성공적으로 완수


🛠 사용 기술 및 도구

TOOL: Github Figma Notion Google spread sheet Adobe Illustrator

WEB FRAMEWORK: REACT JavaScript

LIBRARY: css-module Router


🚀 프로젝트 진행 및 성과

1. 주요 기능 선정: 어플리케이션의 핵심 기능을 결정

페이지 주요 기능 이미지
식재료 관리 식품 등록, 수정, 삭제를 할 수 있습니다
사용자의 식료품을 효율적으로 관리합니다
식품 낭비를 줄이고, 사용자의 경제적 부담을 감소시킵니다

장바구니 마트에서 장을 보기 전 사용자가 구매할 식료품들을 미리 메모할 수 있습니다
온라인 장보기를 위한 대형 온라인 식품 판매 사이트를 연결 해두었습니다
식품 구매의 효율성을 증대시키고 건강한 식습관을 유도합니다

달력 언제 식료품을 보관 했는지 한 눈에 파악 가능합니다
보관한 식료품의 유통기한을 쉽게 확인할 수 있습니다
사용자가 식료품을 안전하게 섭취할 수 있도록 도와줍니다


2. 팀원 및 역할 담당

이성수: 프로젝트 리더, Login.js/ Storage.js/ Food.js 컴포넌트 담당

- 스플래시 화면, 로그인/회원가입, 냉장고 등록/추가/수정/삭제, 식료품 등록/추가/수정/삭제 구현

성영은: 프로젝트 진행 기록, Food.js/ Calendar.js/ Shopping.js 컴포넌트 담당

- 쇼핑리스트, 캘린더 구현

정하솔: Github 관리, 디자인 총괄 담당

- 전체 CSS

조유빈: 디렉토리 관리, 기능 및 디자인 구현 담당

- 전체 CSS


3. 사용자 시나리오 & 워크플로우 & 유스케이스 작성: 사용자의 행동 패턴 및 앱 사용 흐름 도출

싱싱고_워크플로우


4. 요구 사항 분석 : 필요한 기능 및 규격 분석

싱싱고_요구사항


5. 디렉토리 구조 분석 : 프로젝트 구조

싱싱고_디렉토리구조


6. 코딩 및 디자인 가이드 작성 : 개발 및 디자인 표준 설정

코딩 가이드

1. 클래스 네임은 카멜 표기법으로 쉽게 알아볼 수 있는 이름으로 작성하고 네 단어 이상의 사용을 지양한다.
2. const 선언방식을 사용한다.
3. 따옴표는 큰 따옴표를 사용한다.
4. 이벤트 핸들러 함수를 사용한다.
5. Prettier를 사용한다.
6. 설명이 필요한 부분은 주석을 통해 작성한다.
7. 화살표 함수를 사용하여 작성한다.
8. 들여쓰기는 space 2로 통일한다.
9. 특별한 기능이 없는 곳에 사용하는 태그는 `<div>`로 통일한다.

디자인 소개

싱싱고 디자인가이드 싱싱고 디자인가이드2

7. 페이지 기능 구현: 앱 내 각 페이지의 기능을 구현


8. UI/UX 디자인: 사용자 인터페이스 및 디자인 적용

싱싱고전체페이지


9. PPT 제작 및 발표: 앱 주요 기능 소개 및 설명

🎈 프로젝트 회고

문제 및 해결

상황 1

  • 문제 발생:

    데이터를 저장하거나 불러올 방법이 없어 새로고침을 누르거나 창을 나가면 저장했던 데이터가 초기화 되었습니다

  • 원인 파악:

    데이터를 저장하는 로컬 스토리지가 없었습니다

  • 문제 해결:

    로컬 스토리지를 만들고 handleId, handlePassword로 입력한 아이디와 비밀번호를 각각 ID, PW 상태에 저장, useState를 사용하여 ID와 PW를 관리했습니다

1. 페이지가 로드될 때 로컬 스토리지에서 유저 정보를 불러옴

  useEffect(() => {

   // 로컬 스토리지에서 "user" 항목의 값을 가져옴
    const savedUser = localStorage.getItem("user");
    if (savedUser) {
      navigate("/fridge");
    }
  }, [navigate]);

  // ID 입력 값이 변경될 때 호출되는 핸들러
  const handleId = (e) => {
    setId(e.target.value); // 입력 값을 ID 상태에 저장
  };

  // 비밀번호 입력 값이 변경될 때 호출되는 핸들러
  const handlePassword = (e) => {
    setPw(e.target.value); // 입력 값을 비밀번호 상태에 저장
  };

1-1. 로그인 버튼을 클릭했을 때 호출되는 핸들러

  const handleLogin = () => {

   // 로컬 스토리지에서 "users" 항목의 값을 가져와서 파싱
    const users = JSON.parse(localStorage.getItem("users"));
    const user = users?.find(

      // ID와 비밀번호가 모두 일치하는 유저를 찾음
      (user) => user.username === id && user.password === pw
    );

    if (user) { // 일치하는 유저가 있는 경우
      alert("로그인 성공!");

      // 로그인한 유저의 정보를 로컬 스토리지에 저장
      localStorage.setItem("user", JSON.stringify(user));
      navigate("/fridge");
    } else {
      alert("로그인 실패. 아이디 또는 비밀번호를 확인해주세요.");
    }
  };

상황 2

  • 문제 발생:

    캘린더를 통해 지정된 날짜에 등록한 식료품을 나타내는 기능이 구현되지 않았습니다

  • 원인 파악:

    사용 중인 캘린더 플러그인이 우리가 의도한 기능을 구현하지 못하는걸 알게 되었습니다

  • 문제 해결:

    full-calendar 라는 다른 플러그인을 사용하여 플러그인이 가지고 있는 날짜에 따른 이벤트를 띄우는 기능을 통해 원하는 기능을 구현 했습니다

2. FullCalendar 라이브러리 설치

npm install @fullcalendar/react @fullcalendar/daygrid

2-1. FullCalendar에서 사용할 이벤트 배열 생성

const eventArray = foodList.map((item) => {
   const endDay = new Date(item.expirationDate);
   const today = new Date();
   const timeDiff = endDay.getTime() - today.getTime();
   const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));

return {
   category: item.category,
   title: item.foodName,
   start: item.purchaseDate,
   quantity: item.quantity,
   endDay: item.expirationDate,
   dDay: daysDiff >= 0 ? `D-${daysDiff}` : `D+${Math.abs(daysDiff)}`,
     };
  }); setEvents(eventArray);
}, []);

2-2. FullCalendar 컴포넌트를 사용하여 캘린더 렌더링

<FullCalendar
   plugins={[dayGridPlugin, interactionPlugin]} // 사용할 플러그인 목록을 배열로 전달
   initialView="dayGridMonth" // 캘린더가 처음 로드될 때의 뷰 설정
   events={events} // 캘린더에 표시할 이벤트 정보를 배열로 전달
   headerToolbar={{ // 캘린더 상단의 툴바 레이아웃 설정
   left: "prev",
   center: "title",
   right: "next",
   }}

  dateClick={handleDayClick} // 날짜를 클릭했을 때 실행할 함수 전달
  height={400} // 캘린더의 높이 설정
/>

상황 3

  • 문제 발생:

    다른 팀원이 맡은 부분인 Food 페이지에서 식료품 수정을 하고 나서 새로고침을 해야 리스트가 업데이트 되는 점과 수정 시 원래의 값들이 뜨지 않는 문제점들이 생겼습니다

  • 원인 파악:

    원인 파악이 되어있지않아 직접 확인을 해봤어요 수정 컴포넌트를 모달창으로 띄우는 방식에서 데이터를 불러오는 경로가 꼬인거 같다는 생각이 들었습니다

  • 문제 해결:

  • 결국 코드를 싹 다 갈아엎고 처음부터 다시 작성했습니다

  • useEffect Hook을 사용하여 로컬 스토리지와 상태 업데이트 관리했습니다

  • 로컬 스토리지에 'foodList'라는 키로 식품 목록을 문자열 형태로 저장하고 foodList에서 주어진 food와 같은 항목을 찾아 그 인덱스를 반환하며 해결 했습니다

3. useEffect Hook을 사용하여 로컬 스토리지와 상태 업데이트 관리

  useEffect(() => {
    localStorage.setItem("foodList", JSON.stringify(foodList));
    setSortedFoodList(foodList);
  }, [foodList]);

// 식료품 수정 모달 열기 함수
  const openModal = (food, index) => {

    // 수정 중인 음식의 인덱스 및 내용 설정, 모달 열기
    const foodIndex = foodList.findIndex((f) => f === food);
    setEditingIndex(foodIndex);
    setEditingFood({ ...food });
    setIsModalOpen(true);
  };

  // 식료품 수정 모달 닫기 함수
  const closeModal = () => {
    setIsModalOpen(false);
  };

// 변경된 내용 저장 함수
const saveChanges = () => {

  // 수정된 식료품 정보로 목록 업데이트, 모달 닫기, 기존 식료품 목록을 복사하여 새로운 배열을 생성
  const newList = [...foodList];

  // 수정된 식료품을 해당 인덱스에 덮어씌움
  newList[editingIndex] = editingFood;

  // 식료품 목록을 새로운 목록으로 설정
  setFoodList(newList);

  closeModal();
};

3-1. 식료품 수정 모달창

      // isModalOpen이 참일 때만 모달을 표시
      {isModalOpen && (
        <div className={styles.modal}>
          <div className={styles.modalBox}>
            <img
              src={path + "/images/food_back_blue.svg"}
              alt="뒤로가기"
              className={styles.back}
              onClick={closeModal}/>
            <h2>싱싱고 수정하기</h2>

        // setEditingFood 함수를 호출하여 editingFood 상태의 category 값을 업데이트
            <label>
              <select
                value={editingFood.category}
                onChange={(e) =>
                  setEditingFood({ ...editingFood, category: e.target.value })}>
                {categoryOptions.map(
                  (category, index) =>
                    category !== "전체" && (
                      <option key={index} value={category}>
                        {category}
                      </option>
                    ))}
              </select>
            </label>

        // 입력된 값이 변경되면 setEditingFood 함수를 호출하여 수정된 식료품 값으로 업데이트
            <label className={styles.editName}>
              <span>식재료명</span>
              <input
                type="text"
                value={editingFood.foodName}
                onChange={(e) =>
                  setEditingFood({ ...editingFood, foodName: e.target.value })
                }/>
            </label>

        // 입력된 값이 변경되면 setEditingFood 함수를 호출하여 수정된 수량 값으로 업데이트
            <label className={styles.editNum}>
              <span>수량</span>
              <input
                type="number"
                value={editingFood.quantity}
                onChange={(e) =>
                  setEditingFood({ ...editingFood, quantity: e.target.value })
                }
                required/>
            </label>

        // 입력된 값이 변경되면 setEditingFood 함수를 호출하여 수정된 구매일과 소비기간 값으로 업데이트
            <div className={styles.dateBox}>
              <label>
                <span>구매일</span>
                <input
                  type="date"
                  value={editingFood.purchaseDate}
                  onChange={(e) =>
                    setEditingFood({
                      ...editingFood,
                      purchaseDate: e.target.value,
                    })}required/>
              </label>

              <label>
                <span>소비기간</span>
                <input
                  type="date"
                  value={editingFood.expirationDate}
                  onChange={(e) =>
                    setEditingFood({
                      ...editingFood,
                      expirationDate: e.target.value,
                    })}required/>
              </label>

        // 입력된 값이 변경되면 setEditingFood 함수를 호출하여 수정된 메모 값을 업데이트
            </div>
            <label className={styles.editMemo}>
              <span>메모</span>
              <textarea
                value={editingFood.note}
                onChange={(e) =>
                  setEditingFood({ ...editingFood, note: e.target.value })
                }/>
            </label>

상황 4

  • 문제 발생:

    로컬 스토리지로는 각 사용자마다 다른 식료품 목록을 보여주는 구현이 어려웠습니다 ( 저장된 데이터에 다른 데이터를 연결하는 부분이 잘 되지 않았습니다)

  • 원인 파악:

    데이터베이스(ex.몽고DB)를 이용하여 서버 연결을 통해 회원정보와 데이터 정보를 연결해야 했습니다

  • 문제 해결:

    프로젝트가 끝난 후 혼자 독학하며 연결하기로 했습니다

상황 5

  • 문제 발생:

    어색한 분위기에 서로 소통도 잘 안되어 역할을 분담하거나 개개인의 작업 진행 상황을 판단하지 못하였습니다

  • 원인 파악:

    처음 본 사람들끼리 이루어진 팀이라 서로 낯가림이 심했습니다

  • 문제 해결:

    용기를 내 팀원들에게 먼저 다가가 단톡방을 만들고 대화를 주도하여 친밀감을 형성시키고 밝은 분위기로 회의를 이끌어 좋은 분위기에서 프로젝트를 마칠 수 있었습니다

프로젝트 완료 리뷰

  • 아쉬웠던 점:

    1. 리액트를 활용함에 어렵고 서툴렀던 탓에 프로젝트 진행 속도가 늦어졌습니다

    2. 낯을 많이 가리는 성격이라 팀원들과 좋은 팀워크를 이루지 못할 거라 생각했습니다

  • 잘한 점:

    1. 잠을 줄이고 공부 시간을 더 늘려 코딩 능력을 더 향상시키고 리액트를 활용함에 보다 더 능숙해 졌습니다

    2. 처음엔 낯을 좀 가렸지만 용기를 내 팀원들에게 먼저 다가가 결국 팀원들과의 원활한 커뮤니케이션 및 협력을 통해 프로젝트를 성공적으로 완성 시킬 수 있었습니다

  • 배운 점:

    1. 리액트와 관련한 기술적 이해도를 상승시키고 기간 내에 프로젝트를 마무리 지을 수 있었습니다

    2. 처음 만난 사람과의 친밀감을 형성하는 법을 알게 되어 팀원들과의 원활한 커뮤니케이션으로 협업 능력을 향상시켰고 밝은 팀 분위기를 유지하였으며, 팀원들을 이끌 수 있는 리더십 능력을 키울 수 있었습니다

프로젝트 바로가기

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages