시작하며…

시간이 참 빠른 것 같다. 벌써 그룹 스터디가 끝나고 내일이면 두번째 토이프로젝트가 시작하는 날이다. 남은 연말은 프로젝트의 연속일 듯 하여 이번 주말에는 머리를 비우고 그동안 진행했던 사이드프로젝트에 대한 회고를 해볼까 한다.

첫 오프라인 미팅

그룹스터디원끼리는 그동안 온라인으로만 만나다가 프로젝트 기획과 디자인을 위해 오프라인 모임을 갖기로 했다. 익숙한 얼굴들을 실제로 보니 더욱 반가웠다. 근처에서 점심을 먹고 본격적인 작업에 들어갔다. 프로젝트 기획과 디자인부터 진행했는데, 태관님이 알바하실때 필요한 근무 스케줄링 서비스라는 아이디어를 주셔서 UI 디자인을 하며 아이디어를 구체화해보기로 했다. 태관님이 이전에 구상해놓으셨던 UI와 정아님이 사용하시는 “스튜디오메이트”라는 어플을 기반으로 각자 필요하다고 생각되는 페이지의 draft를 그려보고 회의를 통해 필요할 것 같은 페이지 UI를 아래 사진과 같이 확정지었다.

image

솔직히 첫날에 기획과 디자인을 다 할 수 있을것이라 생각조차 못했다. 그동안 나는 프로젝트를 진행하면서 기획과 디자인에 시간 투자를 가장 많이 했기 때문이다. 하지만, 그룹스터디원 모두가 모여서 진행하니 금새 완성할 수 있었고, 그 뒤에는 UI 감각이 뛰어나신 정아님의 공이 컸다. 목표했던 기획과 디자인을 금방 끝내서 모두 기분좋게 저녁 겸 뒷풀이를 진행했다. 나도 오랜만에 새로운 사람들 만나는 자리였다보니 신나서 무리를 해버렸다 ㅎㅎ… (그날 기억이 잘 나지 않는다는 😂)

어쨌든 첫 오프라인 미팅에서 기획과 디자인이라는 목표를 달성하고 본격적인 사이드프로젝트 작업을 시작하였다.

프로젝트 기본 설계

다음 미팅에서는 기본적인 프로젝트 설계를 하였다. 먼저, 각종 레퍼런스들을 살펴보며 기술 스택을 확정지었다. 상태 관리 라이브러리는 용훈님이 강력 추천하신 zustand, UI 라이브러리는 정아님이 추천해주신 ChakraUI를 사용하기로 하였다. 마지막까지 고민했던 것은 JavaScript를 사용할지 TypeScript를 사용할지였다. 결국 이 부분은 결론이 나지 않아 멘토링 시간에 멘토님에게 여쭤보았다. 멘토님은 TypeScript 사용을 추천해주셨지만, 결론적으로 프로젝트를 진행하는 사람들이 해당 기술 스택에 대한 어느 정도의 이해도를 보이는지가 중요하다고 하셔서 결국 이해도가 아직 부족하다고 판단되었던 TypeScript를 배제하고 JavaScript를 채택되었다.

이후, 페이지 UI를 참고하여 폴더 구조를 확정짓고, DB 설계를 했다. FE 개발자들이 모여 DB를 설계하자니 이런저런 고충이 많았다. 특히 어려웠던 점은 우리 프로젝트는 관리자와 스태프로 사용자가 나뉘는데, 각 사용자의 역할에 따라 DB를 구성하려다보니 중복되는 테이블과 필드가 많아졌다. 또한, 어떤 데이터를 참조해야하는지에 대한 기준이 명확하지 않아 불필요한 참조가 발생하였다.

다행히 태관님 지인분들 중 BE 개발자분이 계셔서 그분에게 자문을 구하여 DB 구조를 아래와 같이 확정지었다.

👬 users

id name phone companyId isAdmin gender payPerHour birthDate address
string string string string boolean string number string string

🏢 company

id name code address roles
string string number string array

🦹 bookedShifts

id companyId scheduleId scheduleId
string string array string

🛺 schedule

id companyId date status time numWorkers timestamp
string string object {day, month, year} string object {end, start} number Timestamp

🐓 notice

id title content companyId date timestamp
string string string string date Timestamp

마지막으로, 역할 배정을 진행하였다. 나는 이전부터 로그인 기능을 구현해보고싶어서 팀원들에게 먼저 로그인과 상태관리 파트를 구현해보고싶다고 말씀드렸다. 팀원들도 흔쾌히 허락해주셔서 나는 아래 3가지 기능 구현을 맡게 되었다.

1. 로그인 페이지
2. 인증 관련 로직 구현
2. 유저 상태관리

기능 구현

내가 맡은 페이지의 개수는 로그인 페이지(관리자, 스태프), 정보 입력 페이지(관리자, 스태프), 핀 번호 입력 페이지 이렇게 총 5개였다. 처음에는 상태 관리 라이브러리를 사용하지 않고, useState를 이용하여 상태를 지역적으로 관리하여 임시로 기능 구현을 하였다. 이전에 강의를 들으면서 구현해보았던 부분을 참고하여 구현하다보니 크게 어려움은 없었다.

chakraUI

하지만, 내가 걱정했던 것중 하나는 다름아닌 chakraUI의 사용이었다. 예전부터 css가 가장 쉬우면서 어려워서 새로운 디자인 라이브러리의 사용이 무섭게만 느껴졌다. 하지만, 레퍼런스의 예시를 참고하면서 따라하다보니 styled-component의 사용방법이랑 동일했고, 오히려 아래와 같이 직관적으로 margin이나 size, color 등을 지정할 수 있어서 너무 편했다. 디자인 라이브러리를 왜 쓰는지 알 것 같았다.

import { Button, Heading } from "@chakra-ui/react";

return (
    <style.LoginWrap>
      <Heading as="h2" size="md" mb="1rem">
         일정에 맞게
        <br />
        근무 스케줄 지정
      </Heading>
      <Button
        w="100%"
        mt="100px"
        colorScheme="teal"
        size="md"
        onClick={handleAuth}
      >
        Google 로그인
      </Button>

      ...

Zustand

처음 기능 구현에서는 useState를 이용하여 지역적 상태관리를 하다가 기능 구현을 완료하고, 전역적으로 상태를 관리하기 위하여 Zustand를 사용하여 상태 관리를 해주었다. 실시간 강의에서 Zustand를 배우긴 했지만 간단한 예제들로만 실습을 해본 것이 다였고, 오히려 Redux를 사용하여 상태 관리하는 프로젝트를 만들어 볼 만큼 Redux 외의 다른 상태관리 라이브러리 활용이 걱정되었다. 하지만, Zustand를 사용해보니 금새 그 매력에 빠져버렸다.

import { create } from 'zustand';
import zukeeper from 'zukeeper';
import { persist, createJSONStorage } from 'zustand/middleware';

const useUserStore = create(
  persist(
    (set) => ({
      userData: {
        id: '',
        name: '',
        phone: '',
        companyId: '',
        isAdmin: false,
        gender: '',
        payPerHour: '',
        birthDate: '',
      },
      setUserData: (userData) => set({ userData }),
    }),
    {
      name: 'user',
      storage: createJSONStorage(() => sessionStorage),
    }
  )
);

export default useUserStore;

위와 같이 하나의 store파일 내에서 객체를 생성하고, setter를 설정해준 후 store을 export해주면

const { userData, setUserData } = useUserStore();

위와 같이 구조분해할당을 통해 getter와 setter를 가져와 다른 컴포넌트에서 사용할 수 있다. 얼마나 편한가! Redux가 생각도 나지 않을 만큼 이번 프로젝트에서 Zustand의 매력에 흠뻑 빠져버렸다. 사용 방법도 쉬운 편이라 다른 팀원들도 어렵지 않게 상태를 가져와 사용할 수 있었다.

에러 핸들링

기능 구현을 생각보다 금방 끝내고 자만해있는 사이 어김없이 에러 상황이 발생했다.

전역으로 관리되는 상태의 data와 db에 저장되는 data가 일치하지 않는 에러

처음 기능 구현 시, 로그인을 하면 response를 받아와서 상태를 update해주고, db에 post하여 user 정보를 저장하는 로직을 아래와 같이 구현하였다.

import { GoogleAuthProvider, getAuth, signInWithPopup } from 'firebase/auth';
import useUserStore from "../../store/user/useUserStore";
import { useFireFetch } from "../../hooks/useFireFetch";
import { useNavigate } from "react-router";

...

const provider = new GoogleAuthProvider();
const fireFetch = useFireFetch();
const navigate = useNavigate();
const { userData, setUserData } = useUserStore();

...

if (user?.name) {
  navigate("/dashboard");
}
else {
  signInWithPopup(auth, provider)
    .then((response) => {
      const { uid } = response.user;
      setUserData({ ...userData, id: uid, isAdmin: false });
    })
    .then(() => {
      fireFetch.postData("users", userData.id, userData);
    })
    .then(() => {
      navigate("/info/staff");
    }).catch((error) => {
      console.log(error);
    });
}

...

useEffect(() => {
  const userDataFromLocalStorage = localStorage.getItem("user");
  setUser(JSON.parse(userDataFromLocalStorage));
}, []);

내 의도는 전역 상태로 관리되는 userData 객체에 response에서 받아온 uid값과 isAdmin 속성을 추가하여 상태를 update를 하고, update된 상태를 “users” collection에 post하는 로직을 작성하는 것이었다.

하지만, 위 로직대로 실행하면, update된 userData가 post되는 것이 아니라, 기존에 비어있는 userData객체가 post되는 문제가 발생하였다. .then을 이용하여 순차적으로 비동기 처리가 잘 될 것이라고 기대했지만, 그렇지 않았다.

이에 대하여 용훈님이 주신 솔루션은 아래과 같았다.

...

signInWithPopup(auth, provider).then(async (response) => {
  const { uid } = response.user;

  const Ref = collection(db, 'users');
  const q = query(Ref, where('id', '==', uid));
  const querySnapshot = await getDocs(q);
  const updatedUserData = {
    ...userData,
    id: uid,
    isAdmin: false,
  };

  if (querySnapshot.empty) {
    setUserData(updatedUserData);
    await fireFetch.postData('users', uid, updatedUserData);
    navigate('/info/staff');
  } else {
    querySnapshot.forEach(async (doc) => {
      setUserData({ ...doc.data() });
      navigate('/dashboard');
    });
  }
});

...

바뀐 코드와 이전 코드의 차이점은 크게 두 가지가 있다.

먼저, 상태 update 시, updatedUserData라는 새로운 객체를 만들어서 전달해주었다. userData 객체가 update 되지 않은 상태에서 post됨을 막기 위함이었다.

또한, 기존에는 localStorage를 활용하여 user정보가 존재한다면 dashboard로 이동되는 로직이었다. 하지만, 바뀐 코드에서는 db에 저장된 user정보가 있다면 해당 정보로 상태를 update해준 후 dashboard로의 이동이 일어나고, 그렇지 않다면 updateUserData 객체의 data를 상태로 update하고 그 data를 post해준다.

바뀐 코드의 장점은 localStorage를 사용하기보다는 db의 활용도가 높아졌다는 점, updatedUserData객체를 사용하여 상태 update와 post시 동일한 data를 handling할 수 있다는 점이 있다.

chakraUI 레퍼런스의 부족

해당 에러는 submitCode 페이지를 구성하다가 발생하였다.

위와 같이 pin번호 6자리를 입력받는 form을 구성하면 되는 간단한 페이지 구조였다. pin input에 관한 component가 있는지 chakraUI 레퍼런스를 살펴보았다.

그 사용방법은 아래와 같았다.

<HStack>
  <PinInput>
    <PinInputField />
    <PinInputField />
    <PinInputField />
    <PinInputField />
  </PinInput>
</HStack>

하지만, 입력 받은 후의 pin data를 어떻게 handling하는지에 대해서는 아무 설명이 없었다. 그래서 여느 form과 같이 form 태그로 감싼 후, 버튼을 클릭 시 onSubmit 함수 내에서 data를 handling하려 했다. 하지만 어떤 이유에서인지 form 태그를 이용하면 6자리의 pin 중 한 자리의 pin에 대한 data만 submit되었다.

이 이슈를 해결하기 위해 검색을 해봤다. 검색 결과 field마다 고유한 id값이 존재하지 않아서 발생하는 문제라고 하였다. 결국 문제를 해결하기 위해서는 다음과 같이 비효율적인 코드 작성이 필요하다고 하였다.

import { useForm, Controller } from "react-hook-form";
import {
  Heading,
  PinInput,
  PinInputField,
  Button,
  FormControl,
} from "@chakra-ui/react";

...

const {
    control,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm({ mode: "onBlur" });

...

const onSubmit = (data) => {
    reset();
    const get = async () => {
      if (data) {
        const pinNum = parseInt(
          `${data.pin1}${data.pin2}${data.pin3}${data.pin4}${data.pin5}${data.pin6}`,
          10,
        );

...

<form onSubmit={handleSubmit(onSubmit)}>
  <FormControl display="flex" justifyContent="center" mt="6rem">
    <Controller
      name="pin1"
      control={control}
      render={({ field }) => (
        <PinInput size="lg">
          <PinInputField {...field} />
        </PinInput>
      )}
    />
      <Controller
      name="pin2"
      control={control}
      render={({ field }) => (
        <PinInput size="lg">
          <PinInputField {...field} ml="0.5rem" />
        </PinInput>
      )}
    />

    ...

    <Button type="submit" w="100%" mt="100px" colorScheme="teal" size="md">
      입력
    </Button>
</form>

위 코드처럼 작성 시 단순히 반복되는 코드가 많아진다. 뿐만 아니라, onSubmit함수가 정상적으로 동작을 하기는 하지만 pin 입력 시 Auto Focusing이 되지 않아 일일히 tab 키를 눌러 focusing을 해줘야 하는 문제가 있었다.

결국 리팩토링의 필요성을 느껴 다른 예제를 찾고 찾아봤다. 그 결과, chakraUI를 사용하여 pin을 입력받고 handling하는 예제를 찾을 수 있었다.

function ControlledPinInput() {
  const [value, setValue] = React.useState('');

  const handleChange = (value: string) => {
    setValue(value);
  };

  const handleComplete = (value: string) => {
    console.log(value);
  };
  return (
    <PinInput value={value} onChange={handleChange} onComplete={handleComplete}>
      <PinInputField />
      <PinInputField />
      <PinInputField />
    </PinInput>
  );
}

위 예제처럼 로직을 구성하면 정상적으로 동작이 되었지만, form을 이용해 버튼을 누르면 data를 handling할 수는 없었다. 그래서 UI를 조금 바꿔 버튼을 없애고 pin 6자리가 모두 입력되면 자동으로 handling함수가 호출되는 방식으로 변경하였다.

위 에러를 handling 하며 레퍼런스 코드의 부족함을 느꼈다. chakraUI에서 좀 더 다양한 예제들을 제공했으면 하는 바램이다 ㅎㅎ…

풀리지 않은 미스터리

새로운 관리자 계정을 등록할 때, 관리자가 본인 회사의 이름, 주소, 역할을 등록하면 “company” collection에 company 정보가 post되고, 동시에 “users” collection에서 관리자에 해당하는 문서의 companyId 필드에는 post된 company 문서의 id값이 저장되도록 하는 로직을 구현하려고 하였다.

import { useFireFetch } from "../../../../hooks/useFireFetch";
import { useNavigate } from "react-router";
import useCompanyStore from "../../../../store/company/useCompanyStore";

...

const fireFetch = useFireFetch();
const navigate = useNavigate();
const { companyData, setCompanyData } = useCompanyStore();

...

const onSubmit = async (data) => {
  setRoles(['']);
  reset();

  const updatedCompanyData = {
    name: data.name,
    address: data.address,
    roles: data.roles,
    code: Number(makeRandomCode()),
  };

  setCompanyData(updatedCompanyData);
  fireFetch.addData('company', updatedCompanyData);
  navigate('/dashboard');
};

위 코드는 위에서 말한 기능을 구현하기 전의 로직이다. 기능 구현을 위해 나는 총 4가지 방법으로 시도를 해보았다.

1. store를 이용해서 companyId 가져오기
먼저, 전역으로 관리되고 있는 companyData의 id값을 가져와서 “users” 컬렉션에 update하려고 했다. 하지만, companyData의 최신 상태로 update가 되는 것이 아니라 이전 상태로 update가 되어서 다음 방법을 시도하였다.

2. get 함수를 이용해서 companyId 가져오기
“1번 방법이 먹히지 않으면 db에는 updatedCompanyData 객체의 내용이 저장되어 있겠지? 그러면 get 함수를 이용하여 update된 “company” collection의 id값을 가져오자!” (addData함수에서 id 필드를 만들어 문서의 id값을 저장해준다.) 라는 생각이 들어 해당 로직을 구현하였지만, 확인 결과 받아온 객체에는 id라는 필드가 생성되기 전 data들만 담겨 있었다.

3. query를 이용해 companyId를 가져오기
2번 방법이 먹히지 않자, “그럼 get 함수의 문제인가? query를 짜서 data를 불러와보자!” 라는 생각이 들어 해당 로직을 구현하였지만, 2번 방법을 시도했을 때와 마찬가지로 받아온 객체에는 id라는 필드가 생성되기 전 data들만 담겨 있었다.

4. 비동기 함수인 updateUserData를 호출해 companyId를 가져오기
솔직히 3번 방법까지 안되니까 포기할까 싶었다. “companyId를 수동으로 넣어줘도 되지 않을까?” 라는 생각이 앞섰다. 하지만, 머리를 식힌 후, 마지막으로 아래와 같은 방법을 시도해 보았다.

const updateUserData = async (companyName) => {
  const Ref = collection(db, "company");
  const q = query(Ref, where("name", "==", companyName));
  const querySnapshot = await getDocs(q);

  const id = querySnapshot.docs[0].id;

  const updatedUserData = {
    ...userData,
    companyId: id,
  };

  setUserData(updatedUserData);
  await fireFetch.update("users", userData.id, updatedUserData);
};

const onSubmit = async (data) => {
  ...

  await updateUserData(updatedCompanyData.name);

  ...
};

비동기 함수인 updateUserData를 만들어서 parameter로 updatedCompanyData객체의 name 속성을 전달하여, “company” collection에 해당 문서가 있으면, 문서의 id 필드값을 가져와서 상태를 update하고 “users” collection에 post하는 로직을 구성했다.

결과적으로, 드디어 “company” collection에서 id를 정상적으로 불러와 “users” collection에 post되었다.

const [companyId, setCompanyId] = useState("");

...

const updateUserData = async (companyName) => {
  ...

  if (!querySnapshot.empty) {
    setCompanyId(querySnapshot.docs[0].id);
  }

  ...

하지만, id값을 state로 관리하고 싶어서 위와 같이 로직을 조금 변경하였더니, 또 “company” collection에서 id를 불러오지 못했다.

문제는 해결되었지만 아직도 1, 2, 3번 방법을 시도했을 때, 또, id값을 state로 관리하였을 때 왜 정상적으로 작동하지 않았는지 이해가 되지 않는다. 해당 부분은 조금 더 공부하고 다음에 비슷한 상황이 발생하면 같은 상황이 반복되지 않도록 해야겠다.

salary 페이지 구현

마지막으로, salary 페이지 구현 부분에서 정아님이 에러 핸들링을 요청하셔서 같이 작업을 해보았다. 해당 페이지에서는 “bookedShifts” collection에서 로그인한 계정으로 예약된 스케줄의 id값을 바탕으로 “schedule” collection에서 데이터를 가져와 달력에 표시한다.

해당 달력에 표시된 날짜를 클릭하면 해당 날짜에 “모집 완료” 상태인 스케줄을 “schedule” 컬렉션에서 가져와 근무 기록을 저장한다. 이후, 다시 “bookedShifts” collection에서 가져온 스케줄들의 id에 해당하는 문서를 찾는다. 그 문서들 중 로그인한 계정과 id값이 같고, 근무 기록이 있으며, 배정된 역할이 존재하면 근무 시간을 계산한다.

마지막으로, “users” collection에서 로그인한 계정과 같은 id를 찾아 시급(payPerHour) 정보를 불러온다. 이후, 위에서 계산한 근무 시간 값과 곱해 최종 시급을 계산한다.


위와 같이 상당히 복잡한 기능을 가지고 있어 로직을 구성하는 데에 상당한 시간이 걸렸다. 결론적으로, 나는 중첩 쿼리를 짜서 기능을 구현했지만, 역할이 배정되어 있으면 취소되지 않은 이상 무조건 해당 스케줄은 “모집 완료” 상태라는 점에서 착안하여 단일 쿼리로 구현하신 용훈님의 로직이 salary 페이지에 사용되었다.

하지만, “모집 완료” 상태이긴 하지만 본인의 역할 배정이 완료되지 않은 상태인 “취소됨”까지 고려했을 때는 중첩 쿼리가 필요할 것 같다고 태관님이 말씀하셨고, 무엇보다 반나절동안 고민한 로직이 아쉬워 이렇게 블로그에라도 끄적여보려고 한다.

코드가 워낙 복잡한 만큼, 위에서 설명한 핵심 부분만 포스팅하도록 하겠다.

// "bookedShifts" collection에서 로그인한 계정으로 예약된 스케줄의 id값을 바탕으로 "schedule" collection에서 데이터를 가져오는 hook
useEffect(() => {
  const fetchData = async () => {
    const bookedShiftsRes = await fetch.get('bookedShifts', 'userId', id);

    const isRole = bookedShiftsRes.filter((v, i) => {
      return v.role !== '';
    });

    const matchingSchedules = [];

    for (const v of isRole) {
      const scheduleRes = await fetch.get('schedule', 'id', v.scheduleId);
      if (scheduleRes.length > 0) {
        matchingSchedules.push(...scheduleRes);
      }
    }
    setMyScheduleInfo(matchingSchedules);
  };
  fetchData();
}, []);

// 위의 hook에서 만든 mySchduleInfo 배열을 바탕으로 달력에 스케줄 marking하는 hook
useEffect(() => {
  if (myScheduleInfo[0]) {
    const date = myScheduleInfo.map((obj) => {
      return `${obj.date.year}-${obj.date.month
        .toString()
        .padStart(2, '0')}-${obj.date.day.toString().padStart(2, '0')}`;
    });

    setMark(date);
  }
}, [myScheduleInfo]);

// 달력에 표시된 날짜를 클릭하면 해당 날짜에 "모집 완료" 상태인 스케줄을 "schedule" 컬렉션에서 가져와 근무 기록을 저장.
// 이후, 다시 "bookedShifts" collection에서 가져온 스케줄들의 id에 해당하는 문서를 찾음.
// 그 문서들 중 로그인한 계정과 id값이 같고, 근무 기록이 있으며, 배정된 역할이 존재하면 근무 시간을 계산하는 hook
useEffect(() => {
  const date = myScheduleInfo.map((obj) => {
    return `${obj.date.year}-${obj.date.month
      .toString()
      .padStart(2, '0')}-${obj.date.day.toString().padStart(2, '0')}`;
  });

  const selectedDate = moment(value).format('YYYY-MM-DD');
  if (date.includes(selectedDate)) {
    const indices = date
      .map((el, index) => (el === selectedDate ? index : -1))
      .filter((index) => index !== -1);
    const selected = indices.map((e) => myScheduleInfo[e]);
    setSelectedSchedules(selected);
  } else {
    setSelectedSchedules([]);
  }

  const fetchData = async () => {
    const Ref = collection(db, 'schedule');
    const q = query(Ref, where('status', '==', '모집완료'));
    const querySnapshot = await getDocs(q);
    let workTime = 0;

    if (!querySnapshot.empty) {
      querySnapshot.forEach(async (doc) => {
        const firebaseDate = doc.data().date;

        const dateObj = new Date(
          firebaseDate.year,
          firebaseDate.month - 1,
          firebaseDate.day
        );

        const formattedDate = `${dateObj.getFullYear()}-${(
          dateObj.getMonth() + 1
        )
          .toString()
          .padStart(2, '0')}-${dateObj.getDate().toString().padStart(2, '0')}`;

        if (formattedDate === selectedDate) {
          workTime = doc.data().time;
        }

        const scheduleId = doc.data().id;
        const scheduleRef = collection(db, 'bookedShifts');
        const _q = query(scheduleRef, where('scheduleId', '==', scheduleId));
        const scheduleQuerySnapshot = await getDocs(_q);

        if (!scheduleQuerySnapshot.empty) {
          scheduleQuerySnapshot.forEach(async (doc) => {
            const userId = doc.data().userId;
            const role = doc.data().role;

            if (userId == id && workTime !== 0 && role) {
              setTime(calculateWorkHours(workTime.start, workTime.end));
            }
          });
        }
      });
    }
  };

  fetchData();
}, [value]);

// "users" collection에서 로그인한 계정과 같은 id를 찾아 시급(payPerHour) 정보를 불러옴.
// 이후, 위 hook 계산한 근무 시간 값과 곱해 최종 시급을 계산하는 hook
useEffect(() => {
  const fetchData = async () => {
    const Ref = collection(db, 'users');
    const q = query(Ref, where('id', '==', id));
    const querySnapshot = await getDocs(q);

    if (!querySnapshot.empty) {
      querySnapshot.forEach(async (doc) => {
        const user = doc.data();
        const payPerHour = user.payPerHour;

        const dailyWage = payPerHour * time;

        if (dailyWage !== 0) {
          console.log(`일당: ${dailyWage}원`);
        }
      });
    }
  };

  fetchData();
}, [value]);

// startTime, endTime을 기반으로 근무 시간을 계산해주는 함수
function calculateWorkHours(startTime, endTime) {
  const startParts = startTime.split(':');
  const endParts = endTime.split(':');

  const startHour = parseInt(startParts[0], 10);
  const startMinute = parseInt(startParts[1], 10);
  const endHour = parseInt(endParts[0], 10);
  const endMinute = parseInt(endParts[1], 10);

  const startMinutes = startHour * 60 + startMinute;
  const endMinutes = endHour * 60 + endMinute;

  const minutesWorked = endMinutes - startMinutes;

  const hoursWorked = minutesWorked / 60;

  return hoursWorked;
}

마치며…

드디어 우리 그룹스터디의 처음이자 마지막 목표인 사이드프로젝트를 완료했다. 필수 과제도 아니었지만, 스터디라는 공동의 목적을 위해 시간을 투자하며 열심히 작업해준 태관, 정아, 용훈, 민석님께 다시한번 감사하다는 말씀을 드리고싶다.

또한, 이번 프로젝트에서는 새로운 라이브러리들을 사용하여 보다 쉬우면서 깔끔하게 기능구현을 해볼 수 있어서 좋은 경험이었다. 하지만, 그만큼 새로운 에러들도 많이 마주했고, 지금까지 정확한 원인을 알 수 없는 미스터리한 에러도 있었다. 이런 에러들을 회피하지 않고 끝까지 달라붙어 결국에는 해결한 내 자신에게도 수고했다고 말해주고싶다. 아직은 조금 서툴지 몰라도, 이런 경험이 쌓이고 쌓여 실력 향상의 밑거름이 되리라 굳게 믿는다 🙌🙌

이상 긴 포스팅 읽어주신 모든 분들 감사합니다!

카테고리:

업데이트: