ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

Project

[MBTI-Test PJ_Day 3] Visibility, isOwner

์ฑ„._. 2025. 2. 25. 11:53

ํ•˜๋ฃจ ์ข…์ผ ๊ฑธ๋ ธ๋˜ visibility.. ๊ฟˆ์—์„œ๋„ ๋‚˜์˜ฌ ๋‹จ์–ด

 

2025.02.24 (์›”) Works

 

MBTI ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

๊ฐ™์€ ์„œ๋ฒ„๋ฅผ ์ด์šฉํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์˜ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ž‘์„ฑ์ž๋Š” ์ž์‹ ์˜ ๊ฒฐ๊ณผ๋งŒ์„ ๊ณต๊ฐœ / ๋น„๊ณต๊ฐœ / ์‚ญ์ œ ์ฒ˜๋ฆฌ ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋Š” ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ๋ธŒ๋ผ์šฐ์ €์—๋„ ๋ฐ˜์˜๋˜์–ด์•ผ ํ•˜๋ฏ€๋กœ ํ™”๋ฉด์—์„œ๋งŒ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ DB์— ๋ณ€๋™ ์‚ฌํ•ญ์„ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค.

 

์˜์ƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์‚ญ์ œ ํด๋ฆญ์€ ํ•œ ๋ฒˆ์— ๋ฐ˜์˜๋จ..!

 

 

json-server

 

(ํด๋ฆญ) ์ด์ „์— ์ž‘์„ฑํ•œ JWT ์ธ์ฆ ๋ฐฉ๋ฒ•์€ ํ† ํฐ์„ ์ด์šฉํ•˜์—ฌ auth API์—์„œ ํšŒ์› ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ–ˆ๋‹ค๋ฉด,

ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š” json API๋ฅผ ์ด์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜๊ณ (C), ๊ฐ€์ ธ์˜ค๊ณ (R), ์—…๋ฐ์ดํŠธํ•˜๊ณ (U), ์‚ญ์ œ(D)ํ•ด์•ผํ–ˆ๋‹ค.

 

๋”ฐ๋ผ์„œ ์ƒˆ๋กœ์šด API ๋ฌธ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์—ฐ๊ฒฐ ๋กœ์ง์„ ์ž‘์„ฑํ•˜๊ณ , ํ•ด๋‹น ๋‚ด์šฉ์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

๋”๋ณด๊ธฐ
// theResults.js

import axios from "axios";

const jsonApi = "http://localhost:5000/testResults";

export const getTestResults = async () => {
  const response = await axios.get(jsonApi);
  return response.data;
};

export const createTestResult = async (resultData) => {
  const response = await axios.post(jsonApi, resultData);
  return response.data;
};

export const deleteTestResult = async (id) => {
  const response = await axios.delete(`${jsonApi}/${id}`);
  return response.data;
};

export const updateTestResultVisibility = async (id, visibility) => {
  const response = await axios.patch(`${jsonApi}/${id}`, { visibility });
  return response.data;
};

 

์š”๊ตฌ ์กฐ๊ฑด๊ณผ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

 

์š”๊ตฌ ์กฐ๊ฑด๊ณผ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•ด๋ณด์•˜๋‹ค.

  • ๋‘ ๊ฐœ์˜ ๋ฒ„ํŠผ์€ ์ž‘์„ฑ์ž๋งŒ์ด ๋ณผ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๐Ÿ‘‰๐Ÿป[isOwner] ๊ฒฐ๊ณผ ์นด๋“œ์— ์ €์žฅ๋œ userId์™€ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ userId๋ฅผ ๋น„๊ตํ•˜๊ธฐ
  • ํ˜„์žฌ ํ™”๋ฉด๊ณผ DB ๋ชจ๋‘์—์„œ ์นด๋“œ๋ฅผ ์‚ญ์ œํ•œ๋‹ค. ๐Ÿ‘‰๐Ÿป[visibility] ํ™”๋ฉด: filter ํ•จ์ˆ˜ ์‚ฌ์šฉํ•˜๊ธฐ. DB: delete API ๋กœ์ง ์‚ฌ์šฉํ•˜๊ธฐ
  • ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ๋กœ ์ „ํ™˜๋˜์–ด์•ผ ํ•œ๋‹ค. ๐Ÿ‘‰๐Ÿป[visibility]  visibilty๋กœ ์ „ํ™˜ ์ œ์–ด

 

visibility

๐Ÿค” ๊ณ ๋ฏผ

์ด ๋ชจ๋“  ๊ฒƒ๋“ค์„ ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜๋‚˜...!! ์ •๋ง ๊ณ ๋ฏผ์ด ๋งŽ์•˜์ง€๋งŒ

์šฐ์„  ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์—ฌ๋ถ€๋Š”  ์ƒํƒœ๊ฐ€ ๊ณ„์† ๋ณ€๋™๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

๋”ฐ๋ผ์„œ isVisibility๋ฅผ state๋กœ ๊ด€๋ฆฌํ–ˆ๋‹ค.

setState๋ฅผ ์ด์šฉํ•ด์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ๋‚ด์šฉ์ด ๋ณ€ํ•  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•ด์ฃผ์–ด์•ผ ํ™”๋ฉด์— ์ ์šฉ์ด ๋œ๋‹ค.

  const [isVisibility, setIsVisibility] = useState(false);

 

์ดˆ๊ธฐ๊ฐ’์„ false๋กœ ์ฃผ์–ด ํ…Œ์ŠคํŠธ ๋“ฑ๋ก ์‹œ ๋น„๊ณต๊ฐœ ์ƒํƒœ๋กœ ๋‘์—ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ํŽ˜์ด์ง€์—์„œ ์นด๋“œ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ์—๋„ (createTestResult) false๋กœ ๋‘์—ˆ๋‹ค.

const resultsData = await createTestResult({
      nickname: userInfo.nickname,
      result: mbtiResult,
      visibility: false,
      date: formattedDate,
      userId: userInfo.id,
    });

 

๋‘˜์˜ ์ฐจ์ด๋Š” state : ํ™”๋ฉด์—์„œ ์ „ํ™˜ ๋‹ด๋‹น / create : DB์—์„œ ์ „ํ™˜ ๋‹ด๋‹น ์ด๋‹ค.

์ฆ‰, ํ™”๋ฉด์—์„œ์˜ ๋ณ€ํ™”๋Š” isVisibility๋ฅผ ๋ฐ˜๋Œ€ ๊ฐ’์œผ๋กœ ์ฃผ์–ด์•ผ ํ•˜๊ณ , DB์—์„œ์˜ ๋ณ€ํ™”๋Š” visibility์˜ ๊ฐ’์„ ๋ฐ˜๋Œ€๋กœ ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

 

๐Ÿ“Œ ํ•ด๊ฒฐ

๐Ÿ“ DB์—์„œ ๊ฒฐ๊ณผ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ

๊ณต๊ฐœ ์—ฌ๋ถ€๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค DB์—์„œ ๋ณ€๋™๋œ ๊ฐ’์„ ์ƒˆ๋กœ ๋ถˆ๋Ÿฌ์™€ ๋ฐ˜์˜ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— useEffect ์‚ฌ์šฉ

// DB: ๊ฒฐ๊ณผ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ
  useEffect(() => {
    const fetchData = async () => {
      try {
        const data = await getTestResults();
        setCards(data);
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };
    fetchData();
  }, [isVisibility]);

 

 

๐Ÿ“ ๊ณต๊ฐœ ๋น„๊ณต๊ฐœ ์ „ํ™˜

๋ฒ„ํŠผ์— onClick ์ด๋ฒคํŠธ๋ฅผ ์ฃผ์–ด ํ•ธ๋“ค๋งํ–ˆ๋‹ค.

update- API ๋กœ์ง์„ ์‚ฌ์šฉํ•˜์—ฌ DB์— ๋ฐ˜์˜ํ•˜๊ณ  setState๋ฅผ ์ด์šฉํ•˜์—ฌ ํ™”๋ฉด์— ๋ฐ˜์˜ํ–ˆ๋‹ค.

  // ๊ณต๊ฐœ ๋น„๊ณต๊ฐœ ์ „ํ™˜
  const handleVisibility = async (card) => {
    await updateTestResultVisibility(card.id, !card.visibility);
    setIsVisibility(!isVisibility);
  };

 

 

๐Ÿ“ ๊ณต๊ฐœ ๋น„๊ณต๊ฐœ ๋ฆฌ์ŠคํŠธ

์นด๋“œ ๋ฆฌ์ŠคํŠธ๋„ state๋กœ ๊ด€๋ฆฌ ์ค‘์ด๊ธฐ ๋•Œ๋ฌธ์— ์ „์ฒด cards ๋ฐฐ์—ด์„ filter๋ฉ”์„œ๋“œ๋กœ ์ •๋ ฌํ–ˆ๋‹ค.

|| ๋Š” ์ขŒ์ธก, ์šฐ์ธก ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ๋งŒ์กฑํ•ด๋„ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์—

์นด๋“œ ํ•˜๋‚˜์˜ visibility๊ฐ€ true์ด๊ฑฐ๋‚˜, ์นด๋“œ์˜ userId์™€ ๋กœ๊ทธ์ธ ํ•œ ์‚ฌ์šฉ์ž์˜ id ๊ฐ€ ๊ฐ™์„ ๊ฒฝ์šฐ๋ฅผ ๋‘์—ˆ๋‹ค.

  const [cards, setCards] = useState([]);


// ๊ณต๊ฐœ ๋น„๊ณต๊ฐœ ๋ฆฌ์ŠคํŠธ filter -> ์นด๋“œ ๊ทธ๋ฆฌ๋Š” map์— ์‚ฌ์šฉ
  const viewCards = cards.filter((card) => {
    return card.visibility || card.userId === userInfo.id;
  });

 

 

๐Ÿ“ ์นด๋“œ ์‚ญ์ œ

์œ„์™€ ๊ฐ™์€ confirm ์ฐฝ์„ ๋„์›Œ 'ํ™•์ธ'์„ ํด๋ฆญํ•œ๋‹ค๋ฉด (true๋ผ๋ฉด)

delete- API ๋กœ์ง์„ ์ด์šฉํ•˜์—ฌ DB์— ๋ฐ˜์˜ํ•˜๊ณ  setState๋กœ ํ™”๋ฉด๊นŒ์ง€ ๊ตฌ์„ฑํ•œ๋‹ค.

// ์นด๋“œ ์‚ญ์ œ
  const deleteCard = (cardId) => {
    const alertResult = window.confirm("์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?");
    if (alertResult === true) {
      deleteTestResult(cardId);
      setIsVisibility(!isVisibility);
    }
  };

 

 

 

isOwner

 

isOwner๋Š” ์‚ฌ์šฉ์ž๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ์ž‘์„ฑ์ž์—๊ฒŒ๋งŒ ๋‘ ๋ฒ„ํŠผ์„ ๋ณด์—ฌ์ฃผ๋„๋ก ํ•˜๋Š” ๋ณ€์ˆ˜๋กœ ์ง€์ •ํ•˜์˜€๋‹ค.

 

์ฒ˜์Œ์—๋Š” ํ•จ์ˆ˜๋กœ ๋”ฐ๋กœ ๋นผ๊ณ  ์ด๋ฅผ ๋ฒ„ํŠผ ๋ถ€๋ถ„์— ์ ์šฉํ•˜๋ ค ํ•˜์˜€๋‹ค.

โŒ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•โŒ 

// ์ž‘์„ฑ์ž ๊ธ€์—๋งŒ ๋ฒ„ํŠผ ํ‘œ์‹œํ•˜๊ธฐ
  const isOwner = cards.some((card) => {
    return card.id === userInfo.id;
  });
  console.log("isOwner", isOwner);

  return (
  ...
   {!isOwner ? (
                <></>
              ) : (
                <div className="flex justify-end gap-3 mt-3 mr-5">
                  {card.visibility ? (
                    <button className="px-[20px] py-[8px] text-white bg-blue-500 rounded-full hover:bg-opacity-0 hover:text-blue-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-400 font-semibold hover:bg-primary-dark transition duration-300">
                      ๋น„๊ณต๊ฐœ๋กœ ์ „ํ™˜
                    </button>
                  ) : (
                    <button className="px-[20px] py-[8px] text-white bg-blue-500 rounded-full hover:bg-opacity-0 hover:text-blue-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-400 font-semibold hover:bg-primary-dark transition duration-300">
                      ๊ณต๊ฐœ๋กœ ์ „ํ™˜
                    </button>
                  )}

 

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ๋ชจ๋“  ์นด๋“œ์—์„œ isOwner ===true๊ฐ€ ๋˜์–ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์•˜๋‹ค.
๋”ฐ๋ผ์„œ ๊ฐ ์นด๋“œ(card.id)๊ฐ€ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์œ ์ €(userInfo.id)์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ™•์ธํ•ด์•ผ ํ–ˆ๋‹ค.

 

์•„๋ž˜์™€ ๊ฐ™์ด isOwner๋ฅผ map ๋‚ด๋ถ€์—์„œ ์„ ์–ธํ•˜๊ณ 

&&๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ isOwner๊ฐ€ true ์ผ ๋•Œ (์‚ฌ์šฉ์ž์™€ ์นด๋“œ์˜ id๊ฐ€ ์ผ์น˜ํ•  ๋•Œ) ๋ฒ„ํŠผ์„ ๋ณด์ด๊ฒŒ ํ–ˆ๋‹ค.

{viewCards.map((card) => {
        const isOwner = card.userId === userInfo.id; // ๊ฐœ๋ณ„ ์นด๋“œ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ์ž ํ™•์ธ
        return (
        ...
         {/* ์นด๋“œ์— ์žˆ๋Š” visibility๋กœ ์ž‘์„ฑ์ž์—๊ฒŒ๋งŒ ๋ฒ„ํŠผ ํ‘œ์‹œ */}
                {isOwner && (
                  <div className="flex justify-end gap-3 mt-3 mr-5">
                    {card.visibility ? (
                      <button
                        onClick={() => handleVisibility(card)}
                        className="px-[20px] py-[8px] text-white bg-blue-500 rounded-full hover:bg-opacity-0 hover:text-blue-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-400 font-semibold hover:bg-primary-dark transition duration-300"
                      >
                        ๋น„๊ณต๊ฐœ๋กœ ์ „ํ™˜
                      </button>
                    ) : (
                      <button
                        onClick={() => handleVisibility(card)}
                        className="px-[20px] py-[8px] text-white bg-blue-500 rounded-full hover:bg-opacity-0 hover:text-blue-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-400 font-semibold hover:bg-primary-dark transition duration-300"
                      >
                        ๊ณต๊ฐœ๋กœ ์ „ํ™˜
                      </button>
                    )}
                    <button
                      onClick={() => deleteCard(card.id)}
                      className="px-[20px] py-[8px] text-white bg-red-500 rounded-full hover:bg-opacity-0 hover:text-red-500 focus:outline-2 focus:outline-offset-2 focus:outline-red-400 font-semibold hover:bg-primary-dark transition duration-300"
                    >
                      ์‚ญ์ œ
                    </button>
                  </div>
                )}

 

 

 

๐Ÿ“Œ TestResultList.jsx ์ „์ฒด ๋กœ์ง

 

๋”๋ณด๊ธฐ
๋”๋ณด๊ธฐ
๋”๋ณด๊ธฐ
import { useContext, useEffect, useState } from "react";
import {
  deleteTestResult,
  getTestResults,
  updateTestResultVisibility,
} from "../api/testResults";
import { mbtiDescriptions } from "../utils/mbtiCalculator";
import { AuthContext } from "../context/AuthContext";
import { getUserProfile } from "../api/auth";

const TestResultList = () => {
  const [cards, setCards] = useState([]);
  const [isVisibility, setIsVisibility] = useState(false);
  const { userInfo, setUserInfo, token } = useContext(AuthContext);

  // userInfo ๊ฐ€์ ธ์˜ค๊ธฐ : isOwner์— ํ•„์š”
  useEffect(() => {
    const fetchUserInfo = async () => {
      try {
        const data = await getUserProfile(token);

        setUserInfo({
          id: data.id,
          nickname: data.nickname,
        });
      } catch (error) {
        console.log("Faild to fetch user info", error);
      }
    };
    fetchUserInfo();
  }, []);

  // DB: ๊ฒฐ๊ณผ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ
  useEffect(() => {
    const fetchData = async () => {
      try {
        const data = await getTestResults();
        setCards(data);
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };
    fetchData();
  }, [isVisibility]);

  // ๊ณต๊ฐœ ๋น„๊ณต๊ฐœ ์ „ํ™˜
  const handleVisibility = async (card) => {
    await updateTestResultVisibility(card.id, !card.visibility);
    setIsVisibility(!isVisibility);
  };

  // ๊ณต๊ฐœ ๋น„๊ณต๊ฐœ ๋ฆฌ์ŠคํŠธ filter -> ์นด๋“œ ๊ทธ๋ฆฌ๋Š” map์— ์‚ฌ์šฉ
  const viewCards = cards.filter((card) => {
    return card.visibility || card.userId === userInfo.id;
  });

  // ์นด๋“œ ์‚ญ์ œ
  const deleteCard = (cardId) => {
    const alertResult = window.confirm("์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?");
    if (alertResult === true) {
      deleteTestResult(cardId);
      setIsVisibility(!isVisibility);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center mx-auto md:w-[70%] w-[90%]">
      {cards.length === 0 ? (
        <div className="text-center mt-[80px] mb-[30px]  text-[23px] text-[#475da1]">
          ์•„์ง ๊ณต์œ ๋œ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. <br />
          ๊ฐ€์žฅ ๋จผ์ € mbti ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์œ ํ•˜์„ธ์š” !
        </div>
      ) : (
        <div className="mt-[60px] mb-[30px] text-[27px] font-bold text-[#4b86cd]">
          ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์˜ MBTI๋„ ๊ถ๊ธˆํ•ด !
        </div>
      )}
      {viewCards.map((card) => {
        const isOwner = card.userId === userInfo.id; // ๊ฐœ๋ณ„ ์นด๋“œ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ์ž ํ™•์ธ
        return (
          <div
            key={card.id}
            className="w-[95%] m-[15px] bg-[#474E93] rounded-lg"
          >
            <div className="p-[20px] shadow-lg">
              <div className="flex flex-col justify-between md:flex-row p-2 border-b-[1px]">
                <div className="text-[20px] font-bold text-violet-200">
                  {card.nickname}
                </div>
                <p className="font-light text-violet-200">{card.date}</p>
              </div>
              <div className="py-2">
                <div className="p-2 text-[30px] text-[#fcff97] font-extrabold">
                  {card.result}
                </div>
                <div className="font-light text-violet-200">
                  {mbtiDescriptions[card.result]}
                </div>

                {/* ์นด๋“œ์— ์žˆ๋Š” visibility๋กœ ์ž‘์„ฑ์ž์—๊ฒŒ๋งŒ ๋ฒ„ํŠผ ํ‘œ์‹œ */}
                {isOwner && (
                  <div className="flex justify-end gap-3 mt-3 mr-5">
                    {card.visibility ? (
                      <button
                        onClick={() => handleVisibility(card)}
                        className="px-[20px] py-[8px] text-white bg-blue-500 rounded-full hover:bg-opacity-0 hover:text-blue-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-400 font-semibold hover:bg-primary-dark transition duration-300"
                      >
                        ๋น„๊ณต๊ฐœ๋กœ ์ „ํ™˜
                      </button>
                    ) : (
                      <button
                        onClick={() => handleVisibility(card)}
                        className="px-[20px] py-[8px] text-white bg-blue-500 rounded-full hover:bg-opacity-0 hover:text-blue-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-400 font-semibold hover:bg-primary-dark transition duration-300"
                      >
                        ๊ณต๊ฐœ๋กœ ์ „ํ™˜
                      </button>
                    )}
                    <button
                      onClick={() => deleteCard(card.id)}
                      className="px-[20px] py-[8px] text-white bg-red-500 rounded-full hover:bg-opacity-0 hover:text-red-500 focus:outline-2 focus:outline-offset-2 focus:outline-red-400 font-semibold hover:bg-primary-dark transition duration-300"
                    >
                      ์‚ญ์ œ
                    </button>
                  </div>
                )}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
};

export default TestResultList;
๊ณต์ง€์‚ฌํ•ญ
์ตœ๊ทผ์— ์˜ฌ๋ผ์˜จ ๊ธ€
์ตœ๊ทผ์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€
Total
Today
Yesterday
๋งํฌ
TAG
more
ยซ   2026/03   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
๊ธ€ ๋ณด๊ด€ํ•จ