diff --git a/index.html b/index.html index 5e44ede..42c25b5 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + 숙제노기
diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..85e20fb Binary files /dev/null and b/public/favicon.png differ diff --git a/src/App.tsx b/src/App.tsx index 5d0f508..71748e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import RegisterHomework from './pages/RegisterHomework' import CharacterList from './pages/CharacterList' import HomeworkList from './pages/HomeworkList' import CharacterHomeworkSelect from './pages/CharacterHomeworkSelect' // 정확한 경로로 수정 +import Dashboard from './pages/Dashboard' import Signup from './pages/Signup' const darkTheme = createTheme({ @@ -32,8 +33,9 @@ function App() { } /> } /> } /> - } /> + } /> } /> + } /> } /> diff --git a/src/api/axios.ts b/src/api/axios.ts deleted file mode 100644 index a46b3c9..0000000 --- a/src/api/axios.ts +++ /dev/null @@ -1,17 +0,0 @@ -// src/api/axios.ts -import axios from 'axios'; - -const instance = axios.create({ - baseURL: '/api', // nginx 통해 리버스프록시 -}); - -// 요청 시 자동으로 Authorization 헤더 추가 -instance.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - -export default instance; diff --git a/src/lib/api.ts b/src/lib/api.ts index ab8954f..c9be613 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,7 +1,7 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'http://api.biryu2000.kr:8000', + baseURL: 'http://sukjenogi.biryu2000.kr/api', }) // 요청 시 토큰 자동 추가 diff --git a/src/pages/CharacterHomeworkSelect.tsx b/src/pages/CharacterHomeworkSelect.tsx index 1569a54..3e26a2f 100644 --- a/src/pages/CharacterHomeworkSelect.tsx +++ b/src/pages/CharacterHomeworkSelect.tsx @@ -1,5 +1,14 @@ -import { Box, Card, CardContent, Typography, Grid } from '@mui/material' -import { useNavigate } from 'react-router-dom' +import { + Box, + Card, + CardContent, + Typography, + Grid, + Checkbox, + FormControlLabel, + CircularProgress, + Stack +} from '@mui/material' import { useEffect, useState } from 'react' import api from '../lib/api' @@ -11,10 +20,21 @@ interface Character { combat_power?: number } -export default function CharacterHoCharacterHomeworkSelectmeworkSelect() { - const [characters, setCharacters] = useState([]) - const navigate = useNavigate() +interface Homework { + homework_id: number + title: string + assigned: 'Y' | 'N' + reset_type: string + clear_count: number + loading?: boolean // 체크박스 클릭 처리 중 여부 +} +export default function CharacterHomeworkSelect() { + const [characters, setCharacters] = useState([]) + const [homeworkMap, setHomeworkMap] = useState>({}) + const [loadingMap, setLoadingMap] = useState>({}) + + // 캐릭터 목록 불러오기 useEffect(() => { const fetchCharacters = async () => { try { @@ -27,8 +47,69 @@ export default function CharacterHoCharacterHomeworkSelectmeworkSelect() { fetchCharacters() }, []) - const handleClick = (id: number) => { - navigate(`/characters/${id}/homeworks`) + // 캐릭터별 숙제 불러오기 + useEffect(() => { + characters.forEach(async (char) => { + setLoadingMap(prev => ({ ...prev, [char.id]: true })) + try { + const res = await api.get(`/characterHomework/${char.id}/homeworks/selectable`) + setHomeworkMap(prev => ({ ...prev, [char.id]: res.data })) + } catch (err) { + console.error(`캐릭터 ${char.name}의 숙제를 불러오는 데 실패했습니다.`, err) + } finally { + setLoadingMap(prev => ({ ...prev, [char.id]: false })) + } + }) + }, [characters]) + + // 숙제 지정/해제 + const toggleHomework = async (characterId: number, hw: Homework) => { + const homework_type_id = hw.homework_id + + // 로딩 시작 + setHomeworkMap(prev => ({ + ...prev, + [characterId]: prev[characterId].map(h => + h.homework_id === hw.homework_id ? { ...h, loading: true } : h + ) + })) + + try { + if (hw.assigned === 'Y') { + // 지정 해제 + await api.delete(`/characters/${characterId}/homeworks/${homework_type_id}`) + updateHomeworkState(characterId, homework_type_id, 'N') + } else { + // 숙제 지정 + await api.post(`/characters/${characterId}/homeworks/${homework_type_id}`) + updateHomeworkState(characterId, homework_type_id, 'Y') + } + } catch (err) { + console.error('숙제 상태 변경 실패:', err) + alert('숙제 지정/해제에 실패했습니다.') + } finally { + // 로딩 종료 + setHomeworkMap(prev => ({ + ...prev, + [characterId]: prev[characterId].map(h => + h.homework_id === hw.homework_id ? { ...h, loading: false } : h + ) + })) + } + } + + // 상태만 갱신 + const updateHomeworkState = ( + characterId: number, + homeworkId: number, + assigned: 'Y' | 'N' + ) => { + setHomeworkMap(prev => ({ + ...prev, + [characterId]: prev[characterId].map(h => + h.homework_id === homeworkId ? { ...h, assigned } : h + ) + })) } return ( @@ -39,10 +120,31 @@ export default function CharacterHoCharacterHomeworkSelectmeworkSelect() { {characters.map((char) => ( - handleClick(char.id)} sx={{ cursor: 'pointer' }}> + {char.name} - {/* 숙제 목록이 생기면 여기에 출력 */} + {loadingMap[char.id] ? ( + + ) : ( + + {(homeworkMap[char.id] || []).map(hw => ( + + ) : ( + toggleHomework(char.id, hw)} + /> + ) + } + label={`${hw.title} (${hw.reset_type} ${hw.clear_count}회)`} + /> + ))} + + )} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..61b61ec --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,139 @@ +import { + Box, + Typography, + Card, + CardContent, + Grid, + Stack, + Checkbox +} from '@mui/material' +import { useEffect, useState } from 'react' +import api from '../lib/api' + +interface Character { + character_id: number + character_name: string + server: string +} + +interface Homework { + homework_id: number + title: string + reset_type: string + clear_count: number + complete_cnt: number + loading?: boolean +} + +export default function Dashboard() { + const [characters, setCharacters] = useState([]) + const [homeworks, setHomeworks] = useState>({}) + + useEffect(() => { + const fetchDashboard = async () => { + try { + const res = await api.get('/dashboard/characters') + setCharacters(res.data) + + const homeworkResults = await Promise.all( + res.data.map((char: Character) => + api + .get(`/dashboard/characters/${char.character_id}/homeworks`) + .then(r => ({ id: char.character_id, data: r.data })) + ) + ) + + const map: Record = {} + homeworkResults.forEach(item => { + map[item.id] = item.data + }) + + setHomeworks(map) + } catch (err) { + console.error('대시보드 데이터를 불러오는 데 실패했습니다.', err) + } + } + + fetchDashboard() + }, []) + + const handleCheck = async (characterId: number, hw: Homework, index: number) => { + const newCount = index < hw.complete_cnt ? hw.complete_cnt - 1 : index + 1 + + // 로딩 표시 + setHomeworks(prev => ({ + ...prev, + [characterId]: prev[characterId].map(h => + h.homework_id === hw.homework_id ? { ...h, loading: true } : h + ) + })) + + try { + await api.patch(`/characterHomework/${characterId}/homeworks/${hw.homework_id}`, { + complete_cnt: newCount, + }) + + setHomeworks(prev => ({ + ...prev, + [characterId]: prev[characterId].map(h => + h.homework_id === hw.homework_id + ? { ...h, complete_cnt: newCount, loading: false } + : h + ) + })) + } catch (err) { + console.error('숙제 체크 반영 실패:', err) + alert('숙제 체크 반영에 실패했습니다.') + + setHomeworks(prev => ({ + ...prev, + [characterId]: prev[characterId].map(h => + h.homework_id === hw.homework_id + ? { ...h, loading: false } + : h + ) + })) + } + } + + return ( + + 숙제 대시보드 + + {characters.map(char => ( + + + + + {char.server} : {char.character_name} + + + {(homeworks[char.character_id] || []).map(hw => ( + + + {hw.title} ({hw.clear_count}회) + + + {Array.from({ length: hw.clear_count }).map((_, idx) => ( + + handleCheck(char.character_id, hw, idx) + } + size="small" + /> + ))} + + + ))} + + + + + ))} + + + ) +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 05c53a1..ffe0896 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -2,9 +2,6 @@ import { Box, Typography, Button } from '@mui/material' import { useAuth } from '../contexts/AuthContext' import { Link } from 'react-router-dom' -// 가짜 숙제 데이터는 그대로 유지... -const dummyHomeworks = [ /* ... */ ] - export default function Home() { const { isLoggedIn } = useAuth() @@ -22,20 +19,10 @@ export default function Home() { ) } - // ✅ 로그인 상태일 때 기존 숙제 카드 화면 출력 + // ✅ 로그인 상태일 때 "컨텐츠 들어갈 자리"만 출력 return ( - 이번 주 내 숙제 - - <> - {dummyHomeworks.map(hw => ( - - [{hw.character}] {hw.title} - 상태: {hw.status} - - ))} - - + 컨텐츠 들어갈 자리 ) } diff --git a/src/pages/RegisterHomework.tsx b/src/pages/RegisterHomework.tsx index be2e481..a6a405e 100644 --- a/src/pages/RegisterHomework.tsx +++ b/src/pages/RegisterHomework.tsx @@ -1,7 +1,13 @@ -import { Box, TextField, Button, Typography, MenuItem, Container } from '@mui/material' +import { + Box, + TextField, + Button, + Typography, + MenuItem, + Container +} from '@mui/material' import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import axios from 'axios' import api from '../lib/api' export default function RegisterHomework() { @@ -15,22 +21,14 @@ export default function RegisterHomework() { const handleSubmit = async () => { setError('') try { - await axios({ - method: 'post', - url: 'http://api.biryu2000.kr:8000/homeworks', - headers: { - Authorization: `Bearer ${localStorage.getItem('access_token')}`, - 'Content-Type': 'application/json', - }, - data: { - title, - description, - reset_type: resetType, - clear_count: clearCount, - }, + await api.post('/homeworks', { + title, + description, + reset_type: resetType, + clear_count: clearCount, }) navigate('/homeworks') - } catch (err: any) { + } catch (err) { setError('숙제 등록에 실패했습니다.') console.error(err) } diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx index 714494c..f96d1eb 100644 --- a/src/pages/Signup.tsx +++ b/src/pages/Signup.tsx @@ -19,11 +19,13 @@ export default function Signup() { const [email, setEmail] = useState('') const [emailError, setEmailError] = useState('') const [emailStatus, setEmailStatus] = useState<'available' | 'duplicate' | ''>('') + const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [error, setError] = useState('') const [strengthScore, setStrengthScore] = useState(0) const [strengthLabel, setStrengthLabel] = useState<'약함' | '보통' | '강함' | ''>('') + const [showPassword, setShowPassword] = useState(false) const navigate = useNavigate() @@ -60,7 +62,7 @@ export default function Signup() { } try { - const res = await api.get(`http://api.biryu2000.kr:8000/auth/check-email?email=${value}`) + const res = await api.get(`/auth/check-email?email=${value}`) if (res.data.available === false) { setEmailError('이미 존재하는 이메일입니다.') setEmailStatus('duplicate') @@ -93,7 +95,7 @@ export default function Signup() { } try { - await api.post('http://api.biryu2000.kr:8000/users/', { + await api.post('/users/', { email, password, })