v.1.0
This commit is contained in:
parent
c18f705286
commit
61ddda7fe6
10
src/App.tsx
10
src/App.tsx
@ -10,8 +10,11 @@ import RegisterCharacter from './pages/RegisterCharacter'
|
|||||||
import RegisterHomework from './pages/RegisterHomework'
|
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 HomeworkEditPage from './pages/HomeworkEditPage'
|
||||||
|
import CharacterHomeworkSelect from './pages/CharacterHomeworkSelect'
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
|
import MePage from './pages/MePage'
|
||||||
|
import CharacterEditPage from './pages/CharacterEditPage'
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@ -29,7 +32,7 @@ function App() {
|
|||||||
<Layout key={isLoggedIn ? 'in' : 'out'}>
|
<Layout key={isLoggedIn ? 'in' : 'out'}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/signup" element={<Signup />} />
|
<Route path="/signup" element={<Signup />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/characters/register" element={<RegisterCharacter />} />
|
<Route path="/characters/register" element={<RegisterCharacter />} />
|
||||||
<Route path="/characters" element={<CharacterList />} />
|
<Route path="/characters" element={<CharacterList />} />
|
||||||
@ -38,6 +41,9 @@ function App() {
|
|||||||
<Route path="/characters/:characterId/homeworks" element={<CharacterHomeworkSelect />} />
|
<Route path="/characters/:characterId/homeworks" element={<CharacterHomeworkSelect />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/signup" element={<Signup />} />
|
<Route path="/signup" element={<Signup />} />
|
||||||
|
<Route path="/me" element={<MePage />} />
|
||||||
|
<Route path="/characters/:id/edit" element={<CharacterEditPage />} />
|
||||||
|
<Route path="/homeworks/:id/edit" element={<HomeworkEditPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
import { AppBar, Toolbar, Typography, Button, Menu, MenuItem } from '@mui/material'
|
import {
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
AppBar,
|
||||||
import { useLocation } from 'react-router-dom'
|
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 { useAuth } from '../contexts/AuthContext'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { isLoggedIn, logout } = useAuth()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { isLoggedIn, logout } = useAuth()
|
||||||
|
|
||||||
const [anchorElCharacter, setAnchorElCharacter] = useState<null | HTMLElement>(null)
|
const [anchorElCharacter, setAnchorElCharacter] = useState<null | HTMLElement>(null)
|
||||||
const [anchorElHomework, setAnchorElHomework] = useState<null | HTMLElement>(null)
|
const [anchorElHomework, setAnchorElHomework] = useState<null | HTMLElement>(null)
|
||||||
|
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
|
||||||
const handleLogout = () => {
|
|
||||||
logout()
|
|
||||||
navigate('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMenuOpen = (
|
const handleMenuOpen = (
|
||||||
setter: React.Dispatch<React.SetStateAction<null | HTMLElement>>
|
setter: React.Dispatch<React.SetStateAction<null | HTMLElement>>
|
||||||
@ -29,16 +33,24 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
setter(null)
|
setter(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElUser(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserMenuClose = () => {
|
||||||
|
setAnchorElUser(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout()
|
||||||
|
navigate('/login')
|
||||||
|
}
|
||||||
|
|
||||||
const menuItems = isLoggedIn ? (
|
const menuItems = isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
<Button component={Link} to="/dashboard" color="inherit">대시보드</Button>
|
<Button component={Link} to="/dashboard" color="inherit">대시보드</Button>
|
||||||
|
|
||||||
<Button
|
<Button color="inherit" onClick={handleMenuOpen(setAnchorElCharacter)}>캐릭터</Button>
|
||||||
color="inherit"
|
|
||||||
onClick={handleMenuOpen(setAnchorElCharacter)}
|
|
||||||
>
|
|
||||||
캐릭터
|
|
||||||
</Button>
|
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorElCharacter}
|
anchorEl={anchorElCharacter}
|
||||||
open={Boolean(anchorElCharacter)}
|
open={Boolean(anchorElCharacter)}
|
||||||
@ -48,12 +60,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<MenuItem component={Link} to="/characters/me/homeworks" onClick={handleMenuClose(setAnchorElCharacter)}>내 숙제</MenuItem>
|
<MenuItem component={Link} to="/characters/me/homeworks" onClick={handleMenuClose(setAnchorElCharacter)}>내 숙제</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<Button
|
<Button color="inherit" onClick={handleMenuOpen(setAnchorElHomework)}>숙제</Button>
|
||||||
color="inherit"
|
|
||||||
onClick={handleMenuOpen(setAnchorElHomework)}
|
|
||||||
>
|
|
||||||
숙제
|
|
||||||
</Button>
|
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorElHomework}
|
anchorEl={anchorElHomework}
|
||||||
open={Boolean(anchorElHomework)}
|
open={Boolean(anchorElHomework)}
|
||||||
@ -62,7 +69,21 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<MenuItem component={Link} to="/homeworks" onClick={handleMenuClose(setAnchorElHomework)}>숙제 목록</MenuItem>
|
<MenuItem component={Link} to="/homeworks" onClick={handleMenuClose(setAnchorElHomework)}>숙제 목록</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<Button color="inherit" onClick={handleLogout}>로그아웃</Button>
|
<IconButton color="inherit" onClick={handleUserMenuOpen} size="large">
|
||||||
|
<AccountCircle />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorElUser}
|
||||||
|
open={Boolean(anchorElUser)}
|
||||||
|
onClose={handleUserMenuClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => { handleUserMenuClose(); navigate('/me') }}>
|
||||||
|
내 정보
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => { handleUserMenuClose(); handleLogout() }}>
|
||||||
|
로그아웃
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'https://api.biryu2000.kr',
|
baseURL: 'http://localhost:8000',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 요청 시 토큰 자동 추가
|
// 요청 시 토큰 자동 추가
|
||||||
|
|||||||
110
src/pages/CharacterEditPage.tsx
Normal file
110
src/pages/CharacterEditPage.tsx
Normal file
@ -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 (
|
||||||
|
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>캐릭터 수정</Typography>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="캐릭터 이름"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="서버"
|
||||||
|
value={server}
|
||||||
|
onChange={(e) => setServer(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="전투력"
|
||||||
|
value={combatPower}
|
||||||
|
onChange={(e) => setCombatPower(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="space-between" gap={2}>
|
||||||
|
<Button variant="contained" onClick={handleUpdate}>수정</Button>
|
||||||
|
<Button variant="outlined" color="error" onClick={() => setOpenConfirm(true)}>삭제</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={openConfirm}
|
||||||
|
onClose={() => setOpenConfirm(false)}
|
||||||
|
>
|
||||||
|
<DialogTitle>삭제 확인</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>정말 이 캐릭터를 삭제하시겠습니까?</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpenConfirm(false)}>취소</Button>
|
||||||
|
<Button onClick={handleDelete} color="error">삭제</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -35,7 +35,14 @@ export default function CharacterList() {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{characters.map((char) => (
|
{characters.map((char) => (
|
||||||
<Grid item xs={12} sm={6} md={4} key={char.id} {...({} as any)}>
|
<Grid item xs={12} sm={6} md={4} key={char.id} {...({} as any)}>
|
||||||
<Card>
|
<Card
|
||||||
|
component={Link}
|
||||||
|
to={`/characters/${char.id}/edit`}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">{char.name}</Typography>
|
<Typography variant="h6">{char.name}</Typography>
|
||||||
<Typography color="text.secondary">서버: {char.server || '-'}</Typography>
|
<Typography color="text.secondary">서버: {char.server || '-'}</Typography>
|
||||||
|
|||||||
114
src/pages/HomeworkEditPage.tsx
Normal file
114
src/pages/HomeworkEditPage.tsx
Normal file
@ -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 (
|
||||||
|
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>숙제 수정</Typography>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="숙제 이름"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="설명"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="초기화 주기"
|
||||||
|
value={resetType}
|
||||||
|
onChange={(e) => setResetType(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<MenuItem value="daily">매일</MenuItem>
|
||||||
|
<MenuItem value="weekly">매주</MenuItem>
|
||||||
|
<MenuItem value="monthly">매월</MenuItem>
|
||||||
|
<MenuItem value="none">없음</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
label="최대 클리어 횟수"
|
||||||
|
value={clearCount}
|
||||||
|
onChange={(e) => setClearCount(e.target.value)}
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="space-between" gap={2}>
|
||||||
|
<Button variant="contained" onClick={handleUpdate}>수정</Button>
|
||||||
|
<Button variant="outlined" color="error" onClick={() => setOpenConfirm(true)}>삭제</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Dialog open={openConfirm} onClose={() => setOpenConfirm(false)}>
|
||||||
|
<DialogTitle>숙제 삭제 확인</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>정말 이 숙제를 삭제하시겠습니까?</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpenConfirm(false)}>취소</Button>
|
||||||
|
<Button onClick={handleDelete} color="error">삭제</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -36,7 +36,11 @@ export default function HomeworkList() {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{homeworks.map((hw) => (
|
{homeworks.map((hw) => (
|
||||||
<Grid item key={hw.id} xs={12} sm={6} md={4} {...({} as any)}>
|
<Grid item key={hw.id} xs={12} sm={6} md={4} {...({} as any)}>
|
||||||
<Card>
|
<Card
|
||||||
|
component={Link}
|
||||||
|
to={`/homeworks/${hw.id}/edit`}
|
||||||
|
sx={{ cursor: 'pointer', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">{hw.title}</Typography>
|
<Typography variant="h6">{hw.title}</Typography>
|
||||||
<Typography>주기: {hw.reset_type}</Typography>
|
<Typography>주기: {hw.reset_type}</Typography>
|
||||||
|
|||||||
180
src/pages/MePage.tsx
Normal file
180
src/pages/MePage.tsx
Normal file
@ -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<string | null>(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 (
|
||||||
|
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>내 정보</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="이메일"
|
||||||
|
value={email ?? ''}
|
||||||
|
fullWidth
|
||||||
|
disabled
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="현재 비밀번호"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="새 비밀번호"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
fullWidth
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
const pw = e.target.value
|
||||||
|
setNewPassword(pw)
|
||||||
|
analyzePassword(pw)
|
||||||
|
}}
|
||||||
|
margin="normal"
|
||||||
|
helperText="영문자+숫자 포함 6~20자, 특수문자 허용"
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
error={!!passwordError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{strengthLabel && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
비밀번호 강도: {strengthLabel}
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={(strengthScore / 5) * 100}
|
||||||
|
sx={{
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: '#555',
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
backgroundColor:
|
||||||
|
strengthLabel === '강함' ? '#4caf50' :
|
||||||
|
strengthLabel === '보통' ? '#ff9800' :
|
||||||
|
'#f44336',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="새 비밀번호 확인"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
fullWidth
|
||||||
|
disabled={!!passwordError}
|
||||||
|
>
|
||||||
|
비밀번호 변경
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{(message || passwordError) && (
|
||||||
|
<Typography variant="body2" color="error" sx={{ mt: 2 }}>
|
||||||
|
{message || passwordError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user