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,
})