뽀모도로 구현(2)

차차·2023년 1월 28일
0
post-thumbnail

이전 뽀모도로 문제점

시간 저장이 되지 않는다.

  • 사용자가 url 이동 혹은 브라우저를 껐다가 켰을 때 타이머가 0으로 초기화 되고 카운트가 작동하지 않는다.

이 문제를 해결하기 위해 아래와 같은 기능을 만들고자 한다.

  1. 목표 시간을 localStroage에 저장
  2. 집중 시간과 쉬는 시간을 locaStroage에 저장
  3. 카운트가 진행 중인지 알려주는 상태를 localStroage에 저장
  4. stop을 하였을 때 목표 시간을 (목표시간+ 남은 시간)로 업데이트
  5. stop을 하였을 때 stop 시간을 localStroagedp 저장


집중 시간과 쉬는 시간을 localStroage에 저장

// 집중시간 / 쉬는시간을 입력하고 세팅하는 함수
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!studyRef.current || !restRef.current) return;
    const studyTime = +studyRef.current?.value * 60;
    const restTime = +restRef.current?.value * 60;
    localStorage.setItem("studyTime", studyTime + "");
    localStorage.setItem("restTime", restTime + "");
    setTotalTime(studyTime * 4 + restTime * 3);
    setDefaultTiem({ studyTime, restTime });
  };
// 로컬에 저장된 집중 시간과 쉬는 시간을 불러와 state를 초기화 시킨다.
  useEffect(() => {
    const studyTime = localStorage.getItem("studyTime");
    const restTime = localStorage.getItem("restTime");
    if (!studyTime || !restTime) return;
    setDefaultTiem({ studyTime: +studyTime, restTime: +restTime });
  }, []);

start 버튼을 눌렀을 때 상황에 따른 localStroage 수정 삭제

// isRun을 true false로 토글하는 기능. 32번째 줄은 필요없어 보인다.
  const onClickToggle = useCallback(() => {
    if (cycleRef.current === 0) cycleRef.current = 3;
    const { studyTime, restTime } = defaultTime;
    if (!isRun) {
      // 목표시간을 저장
      localStorage.setItem(
        "targetTime",
        Date.now() + (studyTime * 4 + restTime * 3) * 1000 + ""
      );
      // 진행 상태를 run상태로 변경. stop시간을 제거한다.
      localStorage.setItem("isRun", "1");
      localStorage.removeItem("stopTime");
    } else {
      // 정지시간 저장 / 목표 시간을 다시 정해준다. / 진행 상태를 stop으로 변경한다.
      localStorage.setItem("targetTime", Date.now() + totalTime * 1000 + "");
      localStorage.setItem("stopTime", Date.now() + "");
      localStorage.setItem("isRun", "0");
    }
    setIsRun((prev) => !prev);
  }, [defaultTime, isRun, totalTime]);
  • stopTime이 존재하면 totalTime에 영향을 주기 때문에 start 버튼을 클릭하였을 때 제거한다.
  • stop이 되었을 때 targetTime의 값이 그대로라면 이후에 남은 시간이 변경되지 않는 문제가 생긴다.
    현재 시간에 남은시간을 더해주는 방법으로 targetTime을 재설정한다.

마운트 되었을 때 totalTime을 초기화 해주는 useEffect

// 로컬에 저장된 시간을 불러와 run 상태였다면 실행. stop을 한번이라도 눌렀다면 목표 시간 - 스탑시간으로 계산한다.
  useEffect(() => {
    const targetTime = localStorage.getItem("targetTime");
    const runState = localStorage.getItem("isRun");
    const stopTime = localStorage.getItem("stopTime");
    if (!targetTime || !runState) return;
    if (Boolean(+runState)) setIsRun(true);
    if (!stopTime) return setTotalTime(((+targetTime - Date.now()) / 1000) | 0);
    setTotalTime(((+targetTime - +stopTime) / 1000) | 0);
  }, []);
  • localStroage에 저장된 실행상태가 Boolean(1)이라면 카운트를 작동시킨다.
  • stopTime이 존재하지 않은 경우에는 원래처럼 목표 시간 - 현재 시간을 totalTime에 초기화 시킨다.
  • stopTime이 존재하는 경우에는 목표시간 - 스탑시간을 totalTime에 초기화 시킨다.


전체 코드

import useInterval from "@hooks/useInterval";
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";

type StudyState = "study" | "rest" | "end";

const Timer = () => {
  const [isRun, setIsRun] = useState(false);
  const [defaultTime, setDefaultTiem] = useState({ studyTime: 0, restTime: 0 });
  const [time, setTime] = useState(0);
  const [totalTime, setTotalTime] = useState(0);
  const [studyState, setStudyState] = useState<StudyState>("study");
  const studyRef = useRef<HTMLInputElement>(null);
  const restRef = useRef<HTMLInputElement>(null);
  const cycleRef = useRef<number>(0);

  // 1초마다 실행시킬 함수
  const interValCallback = useCallback(() => {
    setTotalTime((prev) => prev - 1);
  }, []);

  // isRun이 true일 때 interval 실행
  useInterval(isRun, interValCallback);

  // isRun을 true false로 토글하는 기능. 32번째 줄은 필요없어 보인다.
  const onClickToggle = useCallback(() => {
    if (cycleRef.current === 0) cycleRef.current = 3;
    const { studyTime, restTime } = defaultTime;
    if (!isRun) {
      // 목표시간을 저장
      localStorage.setItem(
        "targetTime",
        Date.now() + (studyTime * 4 + restTime * 3) * 1000 + ""
      );
      // 진행 상태와 stop시간을 제거한다.
      localStorage.setItem("isRun", "1");
      localStorage.removeItem("stopTime");
    } else {
      // 정지시간 저장 / 목표 시간을 다시 정해준다. / 진행 상태를 stop으로 변경한다.
      localStorage.setItem("targetTime", Date.now() + totalTime * 1000 + "");
      localStorage.setItem("stopTime", Date.now() + "");
      localStorage.setItem("isRun", "0");
    }
    setIsRun((prev) => !prev);
  }, [defaultTime, isRun, totalTime]);

  // reset 버튼을 클릭했을 때 실행시킬 함수
  const onClickReset = useCallback(() => {
    if (!studyRef.current || !restRef.current) return;
    const studyTime = +studyRef.current?.value;
    const restTime = +restRef.current?.value;
    localStorage.removeItem("targetTime");
    localStorage.removeItem("stopTime");
    setIsRun(false);
    setTotalTime(studyTime * 4 + restTime * 3);
  }, []);

  // 시간을 문자열로 변환하여 00:00처럼 보이게 하는 기능
  const viewTime = useMemo(() => {
    const minutes = (time / 60) | 0;
    const seconds = time % 60;
    return `${minutes < 10 ? "0" + minutes : minutes}:${
      seconds < 10 ? "0" + seconds : seconds
    }`;
  }, [time]);

  // 집중시간 / 쉬는시간을 입력하고 세팅하는 함수
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!studyRef.current || !restRef.current) return;
    // const studyTime = +studyRef.current?.value * 60;
    // const restTime = +restRef.current?.value * 60;
    const studyTime = +studyRef.current?.value;
    const restTime = +restRef.current?.value;
    localStorage.setItem("studyTime", studyTime + "");
    localStorage.setItem("restTime", restTime + "");
    setTotalTime(studyTime * 4 + restTime * 3);
    setDefaultTiem({ studyTime, restTime });
  };

  // 총시간과 디폴트 시간이 변경되었을 때 실행
  useEffect(() => {
    const { studyTime, restTime } = defaultTime;
    if (!studyTime || !restTime) return;
    const sumTime = studyTime + restTime;
    const cycle = ((totalTime + restTime) / sumTime) | 0;
    const remainTime = (totalTime + restTime) % sumTime || sumTime;
    cycleRef.current = cycle;
    if (totalTime === 0) {
      setStudyState("study");
      setTotalTime(studyTime * 4 + restTime * 3);
      setTime(studyTime);
      setIsRun(false);
      localStorage.removeItem("targetTime");
      localStorage.removeItem("stopTime");
      return;
    }
    if (cycle) {
      setTime(
        remainTime <= restTime
          ? remainTime % studyTime || restTime
          : (remainTime - restTime) % studyTime || studyTime
      );
      setStudyState(remainTime <= restTime ? "rest" : "study");
    } else {
      setTime((remainTime - restTime) % studyTime || studyTime);
      setStudyState("study");
    }
  }, [totalTime, defaultTime]);

  // 로컬에 저장된 시간을 불러와 run 상태였다면 실행. stop을 한번이라도 눌렀다면 목표 시간 - 스탑시간으로 계산한다.
  useEffect(() => {
    const targetTime = localStorage.getItem("targetTime");
    const runState = localStorage.getItem("isRun");
    const stopTime = localStorage.getItem("stopTime");
    if (!targetTime || !runState) return;
    if (Boolean(+runState)) setIsRun(true);
    if (!stopTime) return setTotalTime(((+targetTime - Date.now()) / 1000) | 0);
    setTotalTime(((+targetTime - +stopTime) / 1000) | 0);
  }, []);

  // 로컬에 저장된 집중 시간과 쉬는 시간을 불러와 state를 초기화 시킨다.
  useEffect(() => {
    const studyTime = localStorage.getItem("studyTime");
    const restTime = localStorage.getItem("restTime");
    if (!studyTime || !restTime) return;
    setDefaultTiem({ studyTime: +studyTime, restTime: +restTime });
  }, []);

  return (
    <div className="flex-center h-screen">
      <div className="flex w-[30rem] flex-col items-center justify-center space-y-10 bg-red-200 py-[5rem]">
        <span>{studyState}</span>
        <span>{totalTime}</span>
        <span className="text-[2rem]">{viewTime}</span>
        <div className="space-x-3">
          <button
            className="border border-primary-600 p-2 text-[1.6rem]"
            onClick={onClickToggle}
          >
            {isRun ? "stop" : "start"}
          </button>
          <button
            className="border border-primary-600 p-2 text-[1.6rem]"
            onClick={onClickReset}
          >
            reset
          </button>
        </div>
        <form className="flex flex-col space-y-3" onSubmit={onSubmit}>
          <input type="number" placeholder="공부시간" ref={studyRef} />
          <input type="number" placeholder="짧은 휴식" ref={restRef} />
          <button className="w-full border border-primary-700 bg-gray-200 py-3">
            세팅
          </button>
        </form>
      </div>
    </div>
  );
};

export default Timer;

진짜 지금까지 구현했던 기능 중에 가장 머리 쓸일이 많았던 기능같다… 비록 코드는 아주 지저분하지만 ㅠㅠ….
진행률을 알려주는 원형 애니메이션까지 구현하고 이후에 리팩토링을 해야겠다..!!

profile
나는야 프린이

0개의 댓글