From e41bb1dc76900d1d9e83563bf848c82bb3b18b2d Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 8 Nov 2025 15:45:43 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EB=B1=83=EC=A7=80/?= =?UTF-8?q?=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EC=B6=94=EA=B0=80=20=ED=8F=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/badgeApi.js | 16 ++++++ src/components/badge/BadgeForm.jsx | 57 ++++++++++++++++++++++ src/components/badge/BadgeList.jsx | 49 +++++++++++++++++++ src/components/challenge/ChallengeForm.jsx | 57 ++++++++++++++++++++++ src/components/screens/AdminScreen.jsx | 38 +++++++++++++++ src/components/screens/MyPageScreen.jsx | 15 ++++++ src/types/badge.d.ts | 23 +++++++++ 7 files changed, 255 insertions(+) create mode 100644 src/api/badgeApi.js create mode 100644 src/components/badge/BadgeForm.jsx create mode 100644 src/components/badge/BadgeList.jsx create mode 100644 src/components/challenge/ChallengeForm.jsx create mode 100644 src/components/screens/AdminScreen.jsx create mode 100644 src/types/badge.d.ts diff --git a/src/api/badgeApi.js b/src/api/badgeApi.js new file mode 100644 index 0000000..4b619e7 --- /dev/null +++ b/src/api/badgeApi.js @@ -0,0 +1,16 @@ +import api from './axios'; + +export async function getBadges() { + const res = await api.get('/badge'); + return res.data.data; +} + +export async function registerBadge(req) { + const res = await api.post('/badge', req); + return res.data.data; +} + +export async function selectBadge(badgeName) { + const res = await api.get('/badge/select', { params: { badgeName } }); + return res.data.data; +} diff --git a/src/components/badge/BadgeForm.jsx b/src/components/badge/BadgeForm.jsx new file mode 100644 index 0000000..b052317 --- /dev/null +++ b/src/components/badge/BadgeForm.jsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; + +const BadgeForm = () => { + const [name, setName] = useState(''); + const [desc, setDesc] = useState(''); + const [icon, setIcon] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + // TODO: API 연동 + alert(`뱃지 추가: ${name}, 설명: ${desc}, 아이콘: ${icon}`); + setName(''); + setDesc(''); + setIcon(''); + }; + + return ( +
+
+ + setName(e.target.value)} + className='w-full border rounded px-3 py-2' + required + /> +
+
+ + setDesc(e.target.value)} + className='w-full border rounded px-3 py-2' + /> +
+
+ + setIcon(e.target.value)} + className='w-full border rounded px-3 py-2' + /> +
+ +
+ ); +}; + +export default BadgeForm; diff --git a/src/components/badge/BadgeList.jsx b/src/components/badge/BadgeList.jsx new file mode 100644 index 0000000..3b2a270 --- /dev/null +++ b/src/components/badge/BadgeList.jsx @@ -0,0 +1,49 @@ +import React, { useEffect, useState } from 'react'; +import { getBadges, selectBadge } from '../../api/badgeApi'; + +function BadgeListExample() { + const [badges, setBadges] = useState([]); + const [error, setError] = useState(''); + + useEffect(() => { + getBadges().then(setBadges).catch(setError); + }, []); + + const handleSelect = async (name) => { + try { + await selectBadge(name); + setBadges((prev) => + prev.map((b) => ({ + ...b, + isSelected: b.name === name, + })) + ); + } catch (err) { + setError(String(err)); + } + }; + + if (error) return
{error}
; + + return ( + + ); +} + +export default BadgeListExample; diff --git a/src/components/challenge/ChallengeForm.jsx b/src/components/challenge/ChallengeForm.jsx new file mode 100644 index 0000000..2bb0149 --- /dev/null +++ b/src/components/challenge/ChallengeForm.jsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; + +const ChallengeForm = () => { + const [title, setTitle] = useState(''); + const [desc, setDesc] = useState(''); + const [point, setPoint] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + // TODO: API 연동 + alert(`챌린지 추가: ${title}, 설명: ${desc}, 포인트: ${point}`); + setTitle(''); + setDesc(''); + setPoint(''); + }; + + return ( +
+
+ + setTitle(e.target.value)} + className='w-full border rounded px-3 py-2' + required + /> +
+
+ + setDesc(e.target.value)} + className='w-full border rounded px-3 py-2' + /> +
+
+ + setPoint(e.target.value)} + className='w-full border rounded px-3 py-2' + /> +
+ +
+ ); +}; + +export default ChallengeForm; diff --git a/src/components/screens/AdminScreen.jsx b/src/components/screens/AdminScreen.jsx new file mode 100644 index 0000000..d01070b --- /dev/null +++ b/src/components/screens/AdminScreen.jsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import BadgeForm from '../badge/BadgeForm'; +import ChallengeForm from '../challenge/ChallengeForm'; + +const AdminScreen = () => { + const [activeTab, setActiveTab] = useState('badge'); + + return ( +
+

관리자 페이지

+
+ + +
+ {activeTab === 'badge' ? : } +
+ ); +}; + +export default AdminScreen; diff --git a/src/components/screens/MyPageScreen.jsx b/src/components/screens/MyPageScreen.jsx index 2d1efd4..c67375e 100644 --- a/src/components/screens/MyPageScreen.jsx +++ b/src/components/screens/MyPageScreen.jsx @@ -41,6 +41,7 @@ export default function MyPageScreen({ onNavigate }) { (s) => s.user ); const { allBadges, earnedIds } = useSelector((state) => state.badge); + const [isAdmin, setIsAdmin] = useState(true); const [showSetting, setShowSetting] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false); // 로그아웃 모달 상태 @@ -237,6 +238,20 @@ export default function MyPageScreen({ onNavigate }) { 메뉴

diff --git a/src/components/screens/MyPageScreen.jsx b/src/components/screens/MyPageScreen.jsx index c67375e..b0b97b8 100644 --- a/src/components/screens/MyPageScreen.jsx +++ b/src/components/screens/MyPageScreen.jsx @@ -3,6 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { setActiveTab } from '../../store/slices/appSlice'; import { fetchMyPageData, logout } from '../../store/slices/userSlice'; import { calculateEarnedBadges } from '../../store/slices/badgeSlice'; +import api from '../../api/axios'; const themeColor = '#96cb6f'; @@ -41,11 +42,41 @@ export default function MyPageScreen({ onNavigate }) { (s) => s.user ); const { allBadges, earnedIds } = useSelector((state) => state.badge); - const [isAdmin, setIsAdmin] = useState(true); + const [isAdmin, setIsAdmin] = useState(false); const [showSetting, setShowSetting] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false); // 로그아웃 모달 상태 + // 관리자 권한 확인 + const checkAdminStatus = async () => { + const token = localStorage.getItem('token'); + const memberId = localStorage.getItem('memberId'); + + // memberId가 1인 경우만 API 호출 + if (!token || memberId !== '1') { + setIsAdmin(false); + return; + } + + try { + const response = await api.get('/admin', { + headers: { Authorization: `Bearer ${token}` }, + }); + + if ( + response.data.status === 'SUCCESS' && + response.data.data.result + ) { + setIsAdmin(true); + } else { + setIsAdmin(false); + } + } catch (err) { + console.error('관리자 권한 확인 실패', err.response || err); + setIsAdmin(false); + } + }; + // 현재 획득한 최고 레벨 뱃지 찾기 const myBadge = useMemo(() => { const earnedBadges = allBadges.filter((badge) => @@ -65,6 +96,11 @@ export default function MyPageScreen({ onNavigate }) { dispatch(fetchMyPageData()); }, [dispatch]); + // 컴포넌트 마운트 시 관리자 권한 확인 + useEffect(() => { + checkAdminStatus(); + }, []); + useEffect(() => { if (stats.totalPoint !== undefined && stats.totalPoint !== null) { dispatch(calculateEarnedBadges(stats.totalPoint)); @@ -238,7 +274,7 @@ export default function MyPageScreen({ onNavigate }) { 메뉴