v0.10
This commit is contained in:
parent
455887f5ea
commit
a18f66ca7b
@ -2,9 +2,9 @@
|
|||||||
<html lang="en" class="dark">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>숙제노기</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 CharacterList from './pages/CharacterList'
|
||||||
import HomeworkList from './pages/HomeworkList'
|
import HomeworkList from './pages/HomeworkList'
|
||||||
import CharacterHomeworkSelect from './pages/CharacterHomeworkSelect' // 정확한 경로로 수정
|
import CharacterHomeworkSelect from './pages/CharacterHomeworkSelect' // 정확한 경로로 수정
|
||||||
|
import Dashboard from './pages/Dashboard'
|
||||||
import Signup from './pages/Signup'
|
import Signup from './pages/Signup'
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
@ -34,6 +35,7 @@ function App() {
|
|||||||
<Route path="/homeworks" element={<HomeworkList />} />
|
<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="/characters/:characterId/homeworks" element={<CharacterHomeworkSelect />} />
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/signup" element={<Signup />} />
|
<Route path="/signup" element={<Signup />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</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'
|
import axios from 'axios'
|
||||||
|
|
||||||
const api = axios.create({
|
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 {
|
||||||
import { useNavigate } from 'react-router-dom'
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
CircularProgress,
|
||||||
|
Stack
|
||||||
|
} from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import api from '../lib/api'
|
import api from '../lib/api'
|
||||||
|
|
||||||
@ -11,10 +20,21 @@ interface Character {
|
|||||||
combat_power?: number
|
combat_power?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CharacterHoCharacterHomeworkSelectmeworkSelect() {
|
interface Homework {
|
||||||
const [characters, setCharacters] = useState<Character[]>([])
|
homework_id: number
|
||||||
const navigate = useNavigate()
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchCharacters = async () => {
|
const fetchCharacters = async () => {
|
||||||
try {
|
try {
|
||||||
@ -27,8 +47,69 @@ export default function CharacterHoCharacterHomeworkSelectmeworkSelect() {
|
|||||||
fetchCharacters()
|
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 (
|
return (
|
||||||
@ -39,10 +120,31 @@ export default function CharacterHoCharacterHomeworkSelectmeworkSelect() {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{characters.map((char) => (
|
{characters.map((char) => (
|
||||||
<Grid item key={char.id} xs={12} sm={6} md={4}>
|
<Grid item key={char.id} xs={12} sm={6} md={4}>
|
||||||
<Card onClick={() => handleClick(char.id)} sx={{ cursor: 'pointer' }}>
|
<Card sx={{ p: 2 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">{char.name}</Typography>
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</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 { useAuth } from '../contexts/AuthContext'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
// 가짜 숙제 데이터는 그대로 유지...
|
|
||||||
const dummyHomeworks = [ /* ... */ ]
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { isLoggedIn } = useAuth()
|
const { isLoggedIn } = useAuth()
|
||||||
|
|
||||||
@ -22,20 +19,10 @@ export default function Home() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 로그인 상태일 때 기존 숙제 카드 화면 출력
|
// ✅ 로그인 상태일 때 "컨텐츠 들어갈 자리"만 출력
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 4 }}>
|
<Box sx={{ p: 4 }}>
|
||||||
<Typography variant="h5" gutterBottom>이번 주 내 숙제</Typography>
|
<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>
|
|
||||||
</Box>
|
</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 { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
|
||||||
import api from '../lib/api'
|
import api from '../lib/api'
|
||||||
|
|
||||||
export default function RegisterHomework() {
|
export default function RegisterHomework() {
|
||||||
@ -15,22 +21,14 @@ export default function RegisterHomework() {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
await axios({
|
await api.post('/homeworks', {
|
||||||
method: 'post',
|
|
||||||
url: 'http://api.biryu2000.kr:8000/homeworks',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
reset_type: resetType,
|
reset_type: resetType,
|
||||||
clear_count: clearCount,
|
clear_count: clearCount,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
navigate('/homeworks')
|
navigate('/homeworks')
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
setError('숙제 등록에 실패했습니다.')
|
setError('숙제 등록에 실패했습니다.')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,11 +19,13 @@ export default function Signup() {
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [emailError, setEmailError] = useState('')
|
const [emailError, setEmailError] = useState('')
|
||||||
const [emailStatus, setEmailStatus] = useState<'available' | 'duplicate' | ''>('')
|
const [emailStatus, setEmailStatus] = useState<'available' | 'duplicate' | ''>('')
|
||||||
|
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [confirmPassword, setConfirmPassword] = useState('')
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [strengthScore, setStrengthScore] = useState(0)
|
const [strengthScore, setStrengthScore] = useState(0)
|
||||||
const [strengthLabel, setStrengthLabel] = useState<'약함' | '보통' | '강함' | ''>('')
|
const [strengthLabel, setStrengthLabel] = useState<'약함' | '보통' | '강함' | ''>('')
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@ -60,7 +62,7 @@ export default function Signup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
if (res.data.available === false) {
|
||||||
setEmailError('이미 존재하는 이메일입니다.')
|
setEmailError('이미 존재하는 이메일입니다.')
|
||||||
setEmailStatus('duplicate')
|
setEmailStatus('duplicate')
|
||||||
@ -93,7 +95,7 @@ export default function Signup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post('http://api.biryu2000.kr:8000/users/', {
|
await api.post('/users/', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user