This commit is contained in:
SR07 2025-05-16 16:24:17 +09:00
parent 455887f5ea
commit a18f66ca7b
10 changed files with 276 additions and 63 deletions

View File

@ -2,9 +2,9 @@
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>숙제노기</title>
</head>
<body>
<div id="root"></div>

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -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({
@ -34,6 +35,7 @@ function App() {
<Route path="/homeworks" element={<HomeworkList />} />
<Route path="/homeworks/register" element={<RegisterHomework />} />
<Route path="/characters/:characterId/homeworks" element={<CharacterHomeworkSelect />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</Layout>

View File

@ -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;

View File

@ -1,7 +1,7 @@
import axios from 'axios'
const api = axios.create({
baseURL: 'http://api.biryu2000.kr:8000',
baseURL: 'http://sukjenogi.biryu2000.kr/api',
})
// 요청 시 토큰 자동 추가

View File

@ -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<Character[]>([])
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<Character[]>([])
const [homeworkMap, setHomeworkMap] = useState<Record<number, Homework[]>>({})
const [loadingMap, setLoadingMap] = useState<Record<number, boolean>>({})
// 캐릭터 목록 불러오기
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() {
<Grid container spacing={2}>
{characters.map((char) => (
<Grid item key={char.id} xs={12} sm={6} md={4}>
<Card onClick={() => handleClick(char.id)} sx={{ cursor: 'pointer' }}>
<Card sx={{ p: 2 }}>
<CardContent>
<Typography variant="h6">{char.name}</Typography>
{/* 숙제 목록이 생기면 여기에 출력 */}
{loadingMap[char.id] ? (
<CircularProgress size={20} sx={{ mt: 2 }} />
) : (
<Stack spacing={1} mt={2}>
{(homeworkMap[char.id] || []).map(hw => (
<FormControlLabel
key={hw.homework_id}
control={
hw.loading ? (
<CircularProgress size={18} />
) : (
<Checkbox
checked={hw.assigned === 'Y'}
onChange={() => toggleHomework(char.id, hw)}
/>
)
}
label={`${hw.title} (${hw.reset_type} ${hw.clear_count}회)`}
/>
))}
</Stack>
)}
</CardContent>
</Card>
</Grid>

139
src/pages/Dashboard.tsx Normal file
View File

@ -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<Character[]>([])
const [homeworks, setHomeworks] = useState<Record<number, Homework[]>>({})
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<number, Homework[]> = {}
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 (
<Box sx={{ p: 4 }}>
<Typography variant="h5" gutterBottom> </Typography>
<Grid container spacing={2}>
{characters.map(char => (
<Grid item xs={12} sm={6} md={4} key={char.character_id}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{char.server} : {char.character_name}
</Typography>
<Stack spacing={1}>
{(homeworks[char.character_id] || []).map(hw => (
<Box key={hw.homework_id}>
<Typography variant="subtitle2" gutterBottom>
{hw.title} ({hw.clear_count})
</Typography>
<Stack direction="row" spacing={1}>
{Array.from({ length: hw.clear_count }).map((_, idx) => (
<Checkbox
key={idx}
checked={idx < hw.complete_cnt}
disabled={hw.loading}
onChange={() =>
handleCheck(char.character_id, hw, idx)
}
size="small"
/>
))}
</Stack>
</Box>
))}
</Stack>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
)
}

View File

@ -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 (
<Box sx={{ p: 4 }}>
<Typography variant="h5" gutterBottom> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<>
{dummyHomeworks.map(hw => (
<Box key={hw.id} sx={{ width: 300, p: 2, bgcolor: 'background.paper', borderRadius: 2, boxShadow: 3 }}>
<Typography variant="h6">[{hw.character}] {hw.title}</Typography>
<Typography color="text.secondary">: {hw.status}</Typography>
</Box>
))}
</>
</Box>
<Typography variant="h5" gutterBottom> </Typography>
</Box>
)
}

View File

@ -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: {
await api.post('/homeworks', {
title,
description,
reset_type: resetType,
clear_count: clearCount,
},
})
navigate('/homeworks')
} catch (err: any) {
} catch (err) {
setError('숙제 등록에 실패했습니다.')
console.error(err)
}

View File

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