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 ? (
<>
-
+
-
+
-
+
+
+
+
>
) : (
<>
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}}
+
+
+
+
+
+
+
+
+
+
+ )
+}
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}}
+
+
+
+
+
+
+
+
+
+
+ )
+}
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}
+
+ )}
+
+ )
+}