2024-12-23 18:05:37

2024.12.18 (수) ~ 2024.12.20 (금)

 

- 추첨 기록을 밑에 로그 형식으로 남기고자 함

  • 문제점
    • DB를 사용하지 않으므로 로그를 남길 방안이 없음
    • DB를 사용할 경우 이용자가 혹시나 자신의 당첨번호를 회사 관계자가 알 거라는 불안감을 심어줄 수 있음
    • 이에 따라 DB를 사용하지 않는 방법으로 해결을 하고자 진행함

해결 방안

localStorage 사용

Local Storage는 웹 브라우저에서 제공하는 저장소로, 클라이언트 측에서 데이터를 영구적으로 저장할 수 있는 방법

주로 웹 애플리케이션에서 사용자의 데이터를 보관하는 데 사용됨.

  • 특징
    • 영구저장 : 로컬스토리지에 저장된 데이터는 브라우저를 닫거나 컴퓨터를 재부팅해도 사라지지 않음
    • 도메인 기반 : 로컬스토리지는 도메인별로 저장됨, 즉 같은 도메인에서만 접근할 수 있으며, 다른 도메인에서는 해당 데이터에 접근할 수 없음
    • 키-값 쌍 : 데이터는 문자열 형태로 키-값 쌍으로 저장됨, 각 데이터 항목은 고유한 키를 가지고 있으며, 이를 통해 검색하거나 수정할 수 있음
  • 제약사항
    • 용량제한 : 로컬스토리지 용량은 브라우저마다 다르지만, 일반적으로 5MB 정도로 제한되어 있음. 이는 쿠키보다 훨씬 더 많은 데이터를 저장할 수 있는 공간임 -> 또한 우리 데이터의 경우 로또 번호가 전부이기 때문에 5MB 데이터는 충분히 사용하고 남는 용량임
    • 문자열 저장 : 로컬스토리지에 저장되는 데이터는 문자열 형태로만 저장됨. 객체와 배열 등 복잡한 구조는 JSON.stringify()를 사용하여 문자열로 변환한 후 저장해야하며, 다시 사용할 때는 JSON.parse()를 통해 원래 형태로 복원해야 함.
  • 사용예시
    • 사용자 설정 저장 (예, 테마, 언어 선택)
    • 장바구니 정보 저장 (쇼핑 사이트에서)
    • 최근 검색 기록 저장
      • 로컬 스토리지는 사용하기 간편하고, 서버와의 통신 없이 클라이언트에서 데이터를 저장하고 관리할 수 있는 유용한 도구임 -> 그러나 민감한 정보는 보관하지 않는 것이 좋으며, 보안에 유의해야 함.

우리 코드의 경우 Result.tsx 파일을 생성하여 추첨 버튼 클릭 시 결과 값을 불러오고, 데이터는 localStorage로 이동하는 형식으로 방향을 정함

  • 이슈 발생 : 웹에서 보여지는 로그 수를 10개로 정하고 그 이상 추첨 시 밀려나게 설정했으나 고정값으로 들어가는 문제가 있었음 
import React, { useEffect, useState, useRef } from "react";

// SVG 파일 동적 임포트
const svgs = import.meta.glob("../assets/ball_*.svg", {
  eager: true,
}) as Record<string, { default: string }>;

// 숫자를 기반으로 파일 경로를 반환하는 함수
const getImageByNumber = (num: number) => {
  const filePath = `../assets/ball_${num}.svg`;
  return svgs[filePath]?.default || null;
};

interface ResultProps {
  log: number[][]; // 추첨 로그
  onUpdateLog: (newLog: number[][]) => void; // 상위 컴포넌트에서 로그 업데이트를 관리
  isDarkTheme: boolean; // Dark theme flag
}

function Result({ log, isDarkTheme }: ResultProps) {
  const [localLogs, setLocalLogs] = useState<number[][]>([]);
  const [delayedLogs, setDelayedLogs] = useState<number[][]>([]);
  const [visibleLogs, setVisibleLogs] = useState<number[][]>([]);
  const [page, setPage] = useState(1); // 페이지 번호 (5개씩 로드)
  const loaderRef = useRef<HTMLDivElement | null>(null);

  // 로그를 localStorage에서 불러옴
  useEffect(() => {
    const storedLogs = localStorage.getItem("logs");
    if (storedLogs) {
      setLocalLogs(JSON.parse(storedLogs));
    }
  }, []);

  // 새 로그가 추가될 때 2.7초 딜레이 적용 (결과 미리보기 방지)
  useEffect(() => {
    if (log.length === 0) return;

    const delay = setTimeout(() => {
      setDelayedLogs((prevLogs) => {
        const updatedLogs = [...prevLogs, ...log];

        // 중복 제거
        const uniqueLogs = updatedLogs
          .map((item) => JSON.stringify(item)) // 문자열로 변환
          .filter((value, index, self) => self.indexOf(value) === index) // 중복 제거
          .map((item) => JSON.parse(item)); // 다시 객체로 변환

        // localStorage에 저장
        localStorage.setItem("logs", JSON.stringify(uniqueLogs));
        return uniqueLogs;
      });
    }, 2700);

    return () => clearTimeout(delay);
  }, [log]);

  // delayedLogs가 변경될 때, 초기 5개를 설정
  useEffect(() => {
    setVisibleLogs(delayedLogs.slice(0, 5));
  }, [delayedLogs]);

  // Intersection Observer 설정
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const target = entries[0];
        if (target.isIntersecting) {
          setPage((prevPage) => prevPage + 1);
        }
      },
      { threshold: 1.0 } // 완전히 보일 때 트리거
    );

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => {
      if (loaderRef.current) {
        observer.unobserve(loaderRef.current);
      }
    };
  }, []);

  // 페이지 변경될 때마다 visibleLogs 업데이트
  useEffect(() => {
    const newLogs = delayedLogs.slice(0, page * 5);
    setVisibleLogs(newLogs);
  }, [page, delayedLogs]);

  return (
    <>
      <p
        className={`ml-[100px] mt-8 mb-6 text-[25px] font-bold ${
          isDarkTheme ? "text-white" : "text-black"
        }`}
      >
        추첨 기록
      </p>
      <div className="mt-4 mb-8 flex flex-col gap-4 w-[600px] h-[550px] overflow-y-auto rounded-lg bg-gray-100 shadow-lg">
        {visibleLogs
          .slice() // 원본 배열 복사
          .reverse() // 역순 정렬 (최신 기록이 상단)
          .map((draw, drawIndex) => (
            <div
              key={drawIndex}
              className="flex flex-wrap gap-4 justify-center w-full p-4 rounded-lg bg-gray-300 shadow-md"
            >
              {draw.map((num, numIndex) => {
                const imageSrc = getImageByNumber(num);
                return imageSrc ? (
                  <img
                    key={numIndex}
                    src={imageSrc}
                    alt={`Ball ${num}`}
                    className="w-16 h-16"
                  />
                ) : (
                  <div
                    key={numIndex}
                    className="w-16 h-16 flex items-center justify-center bg-gray-100 text-black rounded-full"
                  >
                    {num}
                  </div>
                );
              })}
            </div>
          ))}
        <div ref={loaderRef} className="h-4" /> {/* 무한 스크롤 트리거 */}
      </div>
    </>
  );
}

export default Result;