diff --git a/src/assets/badge/badge-ca-15.png b/src/assets/badge/badge-ca-15.png
new file mode 100644
index 0000000..1e6baac
Binary files /dev/null and b/src/assets/badge/badge-ca-15.png differ
diff --git a/src/assets/badge/badge-ca-3.png b/src/assets/badge/badge-ca-3.png
new file mode 100644
index 0000000..c08fe38
Binary files /dev/null and b/src/assets/badge/badge-ca-3.png differ
diff --git a/src/assets/badge/badge-ca-30.png b/src/assets/badge/badge-ca-30.png
new file mode 100644
index 0000000..39d3de9
Binary files /dev/null and b/src/assets/badge/badge-ca-30.png differ
diff --git a/src/assets/badge/badge-ca-7.png b/src/assets/badge/badge-ca-7.png
new file mode 100644
index 0000000..c13469b
Binary files /dev/null and b/src/assets/badge/badge-ca-7.png differ
diff --git a/src/assets/badge/badge-ta-10.png b/src/assets/badge/badge-ta-10.png
new file mode 100644
index 0000000..c480182
Binary files /dev/null and b/src/assets/badge/badge-ta-10.png differ
diff --git a/src/assets/badge/badge-ta-67.png b/src/assets/badge/badge-ta-67.png
new file mode 100644
index 0000000..56e05a0
Binary files /dev/null and b/src/assets/badge/badge-ta-67.png differ
diff --git a/src/components/BadgeToast.jsx b/src/components/BadgeToast.jsx
new file mode 100644
index 0000000..0033d89
--- /dev/null
+++ b/src/components/BadgeToast.jsx
@@ -0,0 +1,126 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { BADGE_DATA } from './report/badgedata'; // badgedata.js 경로
+
+// ----------------------------------------------------
+// 1. 뱃지 순차 출력(큐)을 관리하는 Custom Hook
+// ----------------------------------------------------
+export const useBadgeToast = () => {
+ const [badgeQueue, setBadgeQueue] = useState([]);
+
+ // 뱃지 달성 이벤트 발생 시 대기열(큐)에 추가
+ const triggerBadge = useCallback((badgeId) => {
+ setBadgeQueue((prev) => [...prev, badgeId]);
+ }, []);
+
+ // 토스트 하나가 완전히 닫히면 대기열에서 완료된 뱃지 제거
+ const handleToastClose = useCallback(() => {
+ setBadgeQueue((prev) => prev.slice(1));
+ }, []);
+
+ // 현재 출력할 뱃지 ID (큐의 첫 번째 항목)
+ const currentBadgeId = badgeQueue[0] || null;
+
+ return {
+ currentBadgeId,
+ triggerBadge,
+ handleToastClose,
+ };
+};
+
+// ----------------------------------------------------
+// 2. 뷰포트 기반 BadgeToast UI 컴포넌트
+// ----------------------------------------------------
+const BadgeToast = ({ badgeId = 'starter', onClose }) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ // BADGE_DATA에서 전달받은 badgeId에 맞는 뱃지 데이터 검색
+ const badgeData = BADGE_DATA.find((badge) => badge.id === badgeId) || BADGE_DATA[0];
+
+ // 수동 닫기 (X 버튼 클릭 시)
+ const handleClose = () => {
+ setIsVisible(false);
+ setTimeout(() => {
+ if (onClose) onClose();
+ }, 600);
+ };
+
+ useEffect(() => {
+ // 1. 마운트 직후 페이드인 & 슬라이드 업
+ const showTimer = setTimeout(() => {
+ setIsVisible(true);
+ }, 50);
+
+ // 2. 5초 유지 후 페이드아웃 시작
+ const hideTimer = setTimeout(() => {
+ setIsVisible(false);
+ }, 5000);
+
+ // 3. 5.6초 후 완전히 종료 콜백 호출 -> 큐에서 다음 뱃지 꺼냄
+ const closeTimer = setTimeout(() => {
+ if (onClose) onClose();
+ }, 5600);
+
+ return () => {
+ clearTimeout(showTimer);
+ clearTimeout(hideTimer);
+ clearTimeout(closeTimer);
+ };
+ }, [badgeId, onClose]);
+
+ return (
+
+
+ {/* X 닫기 버튼 */}
+
+
+ {/* 배지 이미지 영역 */}
+
+

+
+
+ {/* 텍스트 정보 영역 */}
+
+
+ {badgeData.name}
+
+
+ {badgeData.desc}
+
+
+ {badgeData.comment}
+
+
+
+
+ );
+};
+
+export default BadgeToast;
\ No newline at end of file
diff --git a/src/components/SideNav/SideNav.jsx b/src/components/SideNav/SideNav.jsx
index 0d30e91..0725d74 100644
--- a/src/components/SideNav/SideNav.jsx
+++ b/src/components/SideNav/SideNav.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import SettingIcon from "../../assets/SettingIcon.png";
import UserModal from "./UserModal";
+import BadgeToast, { useBadgeToast } from "../BadgeToast"; // 💡 통합된 BadgeToast 및 useBadgeToast import
function SideNav() {
const menuItems = [
@@ -16,6 +17,9 @@ function SideNav() {
const [isModalOpen, setIsModalOpen] = useState(false);
+ // 💡 뱃지 큐 관리를 위한 Custom Hook 호출
+ const { currentBadgeId, triggerBadge, handleToastClose } = useBadgeToast();
+
// 💡 닉네임을 state로 관리하여 변경 감지 가능하게 설정
const [nickname, setNickname] = useState(() => {
return localStorage.getItem('nickname') || '사용자';
@@ -65,6 +69,13 @@ function SideNav() {
};
}, []);
+ // 💡 테스트용: 버튼 클릭 시 뱃지 3개를 연속으로 대기열에 추가 나중에 삭제해야함
+ const handleTestTrigger = () => {
+ triggerBadge('starter');
+ triggerBadge('CA-3');
+ triggerBadge('master');
+ };
+
return (
@@ -75,6 +86,7 @@ function SideNav() {
const isActive = location.pathname === item.path;
return (
+ //테스트용 버튼 삭제해야함
);
})}
+
+ {/* 💡 뱃지 연속 달성 테스트 버튼 */}
+
@@ -121,6 +141,15 @@ function SideNav() {
onClick={handleToggleModal}
/>
+
+ {/* 💡 큐에 대기 중인 뱃지가 있으면 하나씩 순차적으로 출력 */}
+ {currentBadgeId && (
+
+ )}
);
}
diff --git a/src/components/report/badgedata.js b/src/components/report/badgedata.js
index c771f48..cb80332 100644
--- a/src/components/report/badgedata.js
+++ b/src/components/report/badgedata.js
@@ -1,25 +1,179 @@
-import badgeStarter from '../../assets/badge/badge-starter.png';
-import badgeMaster from '../../assets/badge/badge-master.png';
-import badgeTotal500 from '../../assets/badge/badge-total-500.png';
+const images = import.meta.glob('../../assets/badge/*.png', {
+ eager: true,
+ import: 'default'
+});
+
+// 파일명(확장자 제외)만 넣으면 해당 이미지를 반환하는 함수
+const getImg = (name) => images[`../../assets/badge/${name}.png`];
export const BADGE_DATA = [
{
id: 'starter',
- name: '스타터 배지',
- desc: 'CoFix에 오신것을 환영합니다.',
- src: badgeStarter
+ name: 'Hello, World!',
+ desc: 'Cofix 첫 시작',
+ comment: '"Cofix에 오신것을 환영합니다!"',
+ src: getImg('badge-starter')
},
{
id: 'master',
- name: '마스터 배지',
- desc: '모든 배지를 획득하셨습니다!',
- src: badgeMaster
+ name: '진(眞) 고인물',
+ desc: 'Cofix의 모든 배지 획득',
+ comment: '"모든 배지를 획득하셨습니다!"',
+ src: getImg('badge-master')
+ },
+ {
+ id: 'CA-3', //Consecutive Attendance 연속출석
+ name: '또 왔어?',
+ desc: '연속출석 3일 완료하셨습니다!',
+ comment: '"또 오셨네요?"',
+ src: getImg('badge-ca-3')
+ },
+ {
+ id: 'CA-7',
+ name: '단골손님',
+ desc: '연속출석 7일 완료하셨습니다.',
+ comment: '"저희 가게 단골이십니다."',
+ src: getImg('badge-ca-7')
+ },
+ {
+ id: 'CA-15',
+ name: '근면성실 코딩 개미',
+ desc: '연속출석 15일 완료하셨습니다.',
+ comment: '"개미는.. 뚠뚠 오늘도.. 뚠뚠"',
+ src: getImg('badge-ca-15')
+ },
+ {
+ id: 'CA-30',
+ name: '사이버 지박령',
+ desc: '연속출석 30일 완료하셨습니다!',
+ comment: '"당신은 사이버 지박령입니다."',
+ src: getImg('badge-ca-30')
+ },
+ {
+ id: 'TA-10', //Total Attendance 누적출석 약자
+ name: '열 번 찍어 안 넘어 가는 코드 없다.',
+ desc: '누적 출석 10일 완료하셨습니다!',
+ comment: '"10번 찍으면 넘어갈지도.."',
+ src: getImg('badge-ta-10')
+ },
+ {
+ id: 'TA-67',
+ name: '식세븐',
+ desc: '누적 출석 67일 완료하셨습니다!',
+ comment: '"67!!!!!!!!!!!!!!!!!"',
+ src: getImg('badge-ta-67')
+ },
+ {
+ id: 'TA-100',
+ name: '사람이 되었습니다.',
+ desc: '누적 출석 100일 완료하셨습니다!',
+ comment: '"사람되기 이렇게 힘든일이엿나?"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'TA-365',
+ name: '코딩과 물아일체',
+ desc: '누적 출석 365일 완료하셨습니다!',
+ comment: '"깨달았다.. 코딩의 핵심!!"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'TA-777',
+ name: '럭키데이',
+ desc: '누적 출석 777일 완료하셨습니다!',
+ comment: '"뭔가 좋은일이 있으면 좋겠네.."',
+ src: getImg('badge-starter')
},
{
- id: 'total-500',
- name: '오답노트 500회',
- desc: '오답노트 500회 사용하셨습니다!',
- src: badgeTotal500
+ id: 'VD-100', //Vulnerability Detection 약점 발견 약자
+ name: '꼬마 탐정',
+ desc: '약점을 100개 발견하였습니다!',
+ comment: '"약점을 찾았다!!"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'VD-300',
+ name: '견습 탐정',
+ desc: '약점을 300개 발견하였습니다!',
+ comment: '"내 이름은 Co난.."',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'VD-500',
+ name: '유명한 탐정',
+ desc: '약점을 500개 발견하였습니다!',
+ comment: '"내 이름은 유명한.."',
+ src: getImg('badge-total-500')
+ },
+ {
+ id: 'VD-999',
+ name: '비둘기 탐정',
+ desc: '약점을 999개 발견하였습니다!',
+ comment: '"비둘기야 먹자~"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'WR-50', //Weakness Resolution 약점 해결
+ name: '동네 해결사',
+ desc: '약점을 50회 해결했습니다!',
+ comment: '"내가 해결해주마."',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'WR-100',
+ name: '약점엔 쎈 해결사',
+ desc: '약점을 100회 해결했습니다!',
+ comment: '"이것도 해결해 보시지!"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'WR-300',
+ name: '은빛 해결사',
+ desc: '약점을 300회 해결했습니다!',
+ comment: '"더 이상의 허점은 존재하지 않는다."',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'WR-500',
+ name: '은빛 해결사',
+ desc: '약점을 500회 해결했습니다!',
+ comment: '"내가.. 정점에 서겠다."',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'WR-999',
+ name: '비둘기 해결사',
+ desc: '약점을 999회 해결했습니다!',
+ comment: '"비둘기야 먹자~~"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'CD-5', //Coding Duration 누적 코딩시간
+ name: '어린이',
+ desc: '누적 코딩 5시간 달성하셨습니다!',
+ comment: '"벌써 그렇게 됬나?"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'CD-20',
+ name: '청년',
+ desc: '누적 코딩 20시간 달성하셨습니다!',
+ comment: '"코딩했음청년"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'CD-90',
+ name: '코드 깎는 노인',
+ desc: '누적 코딩 90시간 달성하셨습니다!',
+ comment: '"니들이 게 맛을 알어?"',
+ src: getImg('badge-starter')
+ },
+ {
+ id: 'CD-999',
+ name: '코드 깎는 비둘기',
+ desc: '누적 코딩 999시간 달성하셨습니다!',
+ comment: '"비둘기야 먹자~~~"',
+ src: getImg('badge-starter')
},
];
\ No newline at end of file