diff --git a/src/App.tsx b/src/App.tsx index c70167e..b811b1d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,8 +10,11 @@ import RegisterCharacter from './pages/RegisterCharacter' import RegisterHomework from './pages/RegisterHomework' import CharacterList from './pages/CharacterList' import HomeworkList from './pages/HomeworkList' -import CharacterHomeworkSelect from './pages/CharacterHomeworkSelect' // 정확한 경로로 수정 +import HomeworkEditPage from './pages/HomeworkEditPage' +import CharacterHomeworkSelect from './pages/CharacterHomeworkSelect' import Dashboard from './pages/Dashboard' +import MePage from './pages/MePage' +import CharacterEditPage from './pages/CharacterEditPage' const darkTheme = createTheme({ palette: { @@ -29,7 +32,7 @@ function App() { } /> - } /> + } /> } /> } /> } /> @@ -38,6 +41,9 @@ function App() { } /> } /> } /> + } /> + } /> + } /> diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index ff70961..48002a8 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,21 +1,25 @@ -import { AppBar, Toolbar, Typography, Button, Menu, MenuItem } from '@mui/material' -import { Link, useNavigate } from 'react-router-dom' -import { useLocation } from 'react-router-dom' +import { + AppBar, + Toolbar, + Typography, + Button, + Menu, + MenuItem, + IconButton, +} from '@mui/material' +import { AccountCircle } from '@mui/icons-material' +import { Link, useNavigate, useLocation } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' import { useState } from 'react' export default function Layout({ children }: { children: React.ReactNode }) { const location = useLocation() - const { isLoggedIn, logout } = useAuth() const navigate = useNavigate() + const { isLoggedIn, logout } = useAuth() const [anchorElCharacter, setAnchorElCharacter] = useState(null) const [anchorElHomework, setAnchorElHomework] = useState(null) - - const handleLogout = () => { - logout() - navigate('/login') - } + const [anchorElUser, setAnchorElUser] = useState(null) const handleMenuOpen = ( setter: React.Dispatch> @@ -29,16 +33,24 @@ export default function Layout({ children }: { children: React.ReactNode }) { setter(null) } + const handleUserMenuOpen = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget) + } + + const handleUserMenuClose = () => { + setAnchorElUser(null) + } + + const handleLogout = () => { + logout() + navigate('/login') + } + const menuItems = isLoggedIn ? ( <> - + 내 숙제 - + 숙제 목록 - + + + + + { handleUserMenuClose(); navigate('/me') }}> + 내 정보 + + { handleUserMenuClose(); handleLogout() }}> + 로그아웃 + + ) : ( <> diff --git a/src/lib/api.ts b/src/lib/api.ts index a4f4d99..5ae707b 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,7 +1,7 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'https://api.biryu2000.kr', + baseURL: 'http://localhost:8000', }) // 요청 시 토큰 자동 추가 diff --git a/src/pages/CharacterEditPage.tsx b/src/pages/CharacterEditPage.tsx new file mode 100644 index 0000000..793c2ce --- /dev/null +++ b/src/pages/CharacterEditPage.tsx @@ -0,0 +1,110 @@ +import { + Box, + Button, + Container, + TextField, + Typography, + Paper, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle +} from '@mui/material' +import { useEffect, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import api from '../lib/api' + +export default function CharacterEditPage() { + const { id } = useParams() + const navigate = useNavigate() + + const [name, setName] = useState('') + const [server, setServer] = useState('') + const [combatPower, setCombatPower] = useState('') + const [error, setError] = useState('') + const [openConfirm, setOpenConfirm] = useState(false) + + useEffect(() => { + api.get(`/characters/${id}`) + .then(res => { + setName(res.data.name) + setServer(res.data.server || '') + setCombatPower(String(res.data.combat_power || '')) + }) + .catch(() => setError('캐릭터 정보를 불러오는 데 실패했습니다.')) + }, [id]) + + const handleUpdate = async () => { + try { + await api.put(`/characters/${id}`, { + name, + server, + power: Number(combatPower) + }) + navigate('/characters') + } catch { + setError('캐릭터 수정에 실패했습니다.') + } + } + + const handleDelete = async () => { + try { + await api.delete(`/characters/${id}`) + navigate('/characters') + } catch { + setError('캐릭터 삭제에 실패했습니다.') + } + } + + return ( + + 캐릭터 수정 + + + + setName(e.target.value)} + fullWidth + /> + setServer(e.target.value)} + fullWidth + /> + setCombatPower(e.target.value)} + fullWidth + type="number" + /> + + {error && {error}} + + + + + + + + + setOpenConfirm(false)} + > + 삭제 확인 + + 정말 이 캐릭터를 삭제하시겠습니까? + + + + + + + + ) +} diff --git a/src/pages/CharacterList.tsx b/src/pages/CharacterList.tsx index 37960ea..60a2d38 100644 --- a/src/pages/CharacterList.tsx +++ b/src/pages/CharacterList.tsx @@ -35,7 +35,14 @@ export default function CharacterList() { {characters.map((char) => ( - + {char.name} 서버: {char.server || '-'} diff --git a/src/pages/HomeworkEditPage.tsx b/src/pages/HomeworkEditPage.tsx new file mode 100644 index 0000000..d535279 --- /dev/null +++ b/src/pages/HomeworkEditPage.tsx @@ -0,0 +1,114 @@ +import { + Box, Button, Container, Paper, TextField, Typography, MenuItem, + Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions +} from '@mui/material' +import { useEffect, useState } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import api from '../lib/api' + +export default function HomeworkEditPage() { + const { id } = useParams() + const navigate = useNavigate() + + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [resetType, setResetType] = useState('') + const [clearCount, setClearCount] = useState('') + const [error, setError] = useState('') + const [openConfirm, setOpenConfirm] = useState(false) + + useEffect(() => { + api.get(`/homeworks/${id}`) + .then(res => { + const hw = res.data + setTitle(hw.title) + setDescription(hw.description || '') + setResetType(hw.reset_type) + setClearCount(String(hw.clear_count || '')) + }) + .catch(() => setError('숙제 정보를 불러오는 데 실패했습니다.')) + }, [id]) + + const handleUpdate = async () => { + try { + await api.put(`/homeworks/${id}`, { + name: title, // ✅ title → name + description, + repeat_type: resetType, // ✅ reset_type → repeat_type + repeat_count: Number(clearCount) // ✅ clear_count → repeat_count + }) + navigate('/homeworks') + } catch { + setError('숙제 수정에 실패했습니다.') + } + } + + const handleDelete = async () => { + try { + await api.delete(`/homeworks/${id}`) + navigate('/homeworks') + } catch { + setError('숙제 삭제에 실패했습니다.') + } + } + + return ( + + 숙제 수정 + + + + setTitle(e.target.value)} + fullWidth + /> + setDescription(e.target.value)} + fullWidth + /> + setResetType(e.target.value)} + fullWidth + > + 매일 + 매주 + 매월 + 없음 + + setClearCount(e.target.value)} + type="number" + fullWidth + /> + + {error && {error}} + + + + + + + + + setOpenConfirm(false)}> + 숙제 삭제 확인 + + 정말 이 숙제를 삭제하시겠습니까? + + + + + + + + ) +} diff --git a/src/pages/HomeworkList.tsx b/src/pages/HomeworkList.tsx index 3f4ffc2..3891940 100644 --- a/src/pages/HomeworkList.tsx +++ b/src/pages/HomeworkList.tsx @@ -36,7 +36,11 @@ export default function HomeworkList() { {homeworks.map((hw) => ( - + {hw.title} 주기: {hw.reset_type} diff --git a/src/pages/MePage.tsx b/src/pages/MePage.tsx new file mode 100644 index 0000000..6c166ae --- /dev/null +++ b/src/pages/MePage.tsx @@ -0,0 +1,180 @@ +import { + Box, + Button, + Container, + TextField, + Typography, + InputAdornment, + IconButton, + LinearProgress, +} from '@mui/material' +import Visibility from '@mui/icons-material/Visibility' +import VisibilityOff from '@mui/icons-material/VisibilityOff' +import { useEffect, useState } from 'react' +import api from '../lib/api' + +export default function MePage() { + const [email, setEmail] = useState('') + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + + const [passwordError, setPasswordError] = useState('') + const [message, setMessage] = useState(null) + const [showPassword, setShowPassword] = useState(false) + + const [strengthScore, setStrengthScore] = useState(0) + const [strengthLabel, setStrengthLabel] = useState<'약함' | '보통' | '강함' | ''>('') + + useEffect(() => { + api.get('/users/me') + .then(res => setEmail(res.data.email || '')) + .catch(() => setMessage('내 정보를 불러오는데 실패했습니다.')) + }, []) + + const analyzePassword = (pw: string) => { + let score = 0 + if (/[a-z]/.test(pw)) score++ + if (/[A-Z]/.test(pw)) score++ + if (/\d/.test(pw)) score++ + if (/[^A-Za-z0-9]/.test(pw)) score++ + if (pw.length >= 10) score++ + + setStrengthScore(score) + + if (score <= 2) setStrengthLabel('약함') + else if (score <= 4) setStrengthLabel('보통') + else setStrengthLabel('강함') + } + + const validatePassword = (pw: string) => { + const regex = /^(?=.*[A-Za-z])(?=.*\d).{6,20}$/ + return regex.test(pw) + } + + const handleSubmit = async () => { + setPasswordError('') + setMessage(null) + + if (!validatePassword(newPassword)) { + setPasswordError('비밀번호는 영문자+숫자를 포함한 6~20자여야 합니다.') + return + } + + if (newPassword !== confirmPassword) { + setPasswordError('비밀번호가 일치하지 않습니다.') + return + } + + try { + await api.put('/users/me/password', { + current_password: currentPassword, + new_password: newPassword + }) + setMessage('비밀번호가 성공적으로 변경되었습니다.') + setCurrentPassword('') + setNewPassword('') + setConfirmPassword('') + setStrengthScore(0) + setStrengthLabel('') + } catch { + setMessage('비밀번호 변경에 실패했습니다.') + } + } + + return ( + + 내 정보 + + + + setCurrentPassword(e.target.value)} + margin="normal" + /> + + { + const pw = e.target.value + setNewPassword(pw) + analyzePassword(pw) + }} + margin="normal" + helperText="영문자+숫자 포함 6~20자, 특수문자 허용" + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} edge="end"> + {showPassword ? : } + + + ), + }} + error={!!passwordError} + /> + + {strengthLabel && ( + + + 비밀번호 강도: {strengthLabel} + + + + )} + + setConfirmPassword(e.target.value)} + margin="normal" + /> + + + + {(message || passwordError) && ( + + {message || passwordError} + + )} + + ) +}