v0.10
This commit is contained in:
parent
455887f5ea
commit
a18f66ca7b
@ -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
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -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() {
|
||||
<Route path="/characters/register" element={<RegisterCharacter />} />
|
||||
<Route path="/characters" element={<CharacterList />} />
|
||||
<Route path="/homeworks" element={<HomeworkList />} />
|
||||
<Route path="/homeworks/register" element={<RegisterHomework />} />
|
||||
<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>
|
||||
|
||||
@ -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;
|
||||
@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://api.biryu2000.kr:8000',
|
||||
baseURL: 'http://sukjenogi.biryu2000.kr/api',
|
||||
})
|
||||
|
||||
// 요청 시 토큰 자동 추가
|
||||
|
||||
@ -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
139
src/pages/Dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user