Compare commits

...

42 Commits

Author SHA1 Message Date
68ae7eb6fd
Merge pull request #18 from nightbug-xx/yiuc1w-codex/친구정보-출력-문제-수정
Improve friend list layout
2025-06-11 11:35:27 +09:00
11fcf65d11 docs: update usage guide and release notes 2025-06-11 11:35:03 +09:00
056e2b5589
Merge pull request #17 from nightbug-xx/hp4atl-codex/친구정보-출력-문제-수정
Improve friend list layout
2025-06-11 11:29:44 +09:00
bfd04fc68c Style friend list with cards 2025-06-11 11:29:27 +09:00
3707fda83a
Merge pull request #16 from nightbug-xx/mct40z-codex/친구정보-출력-문제-수정
Add friend dashboard page
2025-06-11 11:14:55 +09:00
81b1bb0a75
Merge branch 'main' into mct40z-codex/친구정보-출력-문제-수정 2025-06-11 11:14:47 +09:00
6524ce6cd8 Handle complete_cnt in friend homework API 2025-06-11 11:13:25 +09:00
9d6bef7c64
Merge pull request #15 from nightbug-xx/mcr5z4-codex/친구정보-출력-문제-수정
Add friend dashboard page
2025-06-11 11:00:21 +09:00
3e82a68e2c
Merge branch 'main' into mcr5z4-codex/친구정보-출력-문제-수정 2025-06-11 11:00:14 +09:00
8b2ab534d3 Update friend dashboard to new API shapes 2025-06-11 10:58:36 +09:00
ed53cfcc11
Merge pull request #14 from nightbug-xx/n28iag-codex/친구정보-출력-문제-수정
Add friend dashboard page
2025-06-11 10:48:48 +09:00
6751c9094a
Merge branch 'main' into n28iag-codex/친구정보-출력-문제-수정 2025-06-11 10:48:18 +09:00
6b2318a62a Update friend dashboard route and cleanup 2025-06-11 10:46:29 +09:00
f554241cd8
Merge pull request #13 from nightbug-xx/tom6ed-codex/친구정보-출력-문제-수정
Add friend dashboard page
2025-06-11 10:39:37 +09:00
21a284b815 Add friend dashboard page 2025-06-11 10:39:20 +09:00
dc1c120837
Merge pull request #12 from nightbug-xx/31gcf4-codex/친구정보-출력-문제-수정
Add public visibility toggles
2025-06-11 10:28:10 +09:00
8457f4cbee
Merge branch 'main' into 31gcf4-codex/친구정보-출력-문제-수정 2025-06-11 10:28:02 +09:00
0c537df8a7 Align request payloads with API spec 2025-06-11 10:21:57 +09:00
02a9847207
Merge pull request #11 from nightbug-xx/zn9csj-codex/친구정보-출력-문제-수정
Add public visibility toggles
2025-06-11 10:10:06 +09:00
1d40784572 Add public visibility option to character and homework forms 2025-06-11 10:09:36 +09:00
2a18572e59
Merge pull request #10 from nightbug-xx/z1auo3-codex/친구정보-출력-문제-수정
Fix friend list fetching
2025-06-10 10:24:37 +09:00
45a9f5da5a Update friend list fetching 2025-06-10 10:22:49 +09:00
bdb8c22ec1
Merge pull request #9 from nightbug-xx/codex/친구정보-출력-문제-수정
Fix friend request list user info
2025-06-10 10:04:01 +09:00
9ed763b5fe Fix friend request list user info 2025-06-10 10:03:40 +09:00
dbfc2faaa9
Merge pull request #8 from nightbug-xx/codex/푸시못한-파일-올리기
Add friend routes
2025-06-09 18:58:11 +09:00
39187938f0 Add missing friend routes 2025-06-09 18:57:54 +09:00
SR07
ae2a17ca5c Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/App.tsx
#	src/lib/api.ts
2025-06-09 18:52:45 +09:00
SR07
273b76ea77 친구 작업중 2025-06-09 18:52:13 +09:00
e1fb81bf4e
Merge pull request #7 from nightbug-xx/codex/상단-메뉴에-친구-메뉴-추가
Add friend menu with submenus
2025-06-09 18:51:32 +09:00
91f5a32dbe Add friend menu with submenus 2025-06-09 18:51:14 +09:00
ee6535188c
Merge pull request #6 from nightbug-xx/codex/정리-src/index.css-및-import-추가
Remove unused index.css
2025-06-09 18:43:38 +09:00
13ffffd8cd
Merge pull request #5 from nightbug-xx/codex/remove-unused-token_type-and-clean-up-logic
Remove unused token_type logic
2025-06-09 18:43:29 +09:00
6302c85d0f
Merge pull request #4 from nightbug-xx/codex/configure-api-server-url-in-environment-files
feat: load API base URL from env
2025-06-09 18:43:19 +09:00
2667faae28
Merge pull request #3 from nightbug-xx/codex/수정된-내-숙제-메뉴-항목-링크-확인
Fix My Homework link
2025-06-09 18:43:08 +09:00
867be4e3ba
Merge pull request #2 from nightbug-xx/codex/change-power-to-combat_power-in-charactereditpage
Fix edit payload field
2025-06-09 18:42:56 +09:00
70529fab2b
Merge pull request #1 from nightbug-xx/codex/중복된-route-삭제
Fix duplicated signup route
2025-06-09 18:42:46 +09:00
eb8de2b697 불필요한 index.css 제거 및 main.tsx 정리 2025-06-09 18:42:20 +09:00
d2efeee720 Remove unused token_type logic 2025-06-09 18:41:32 +09:00
0d52ef187a feat: load API base URL from env 2025-06-09 18:41:11 +09:00
908196cacd Add route for my homeworks 2025-06-09 18:40:39 +09:00
bf26a295be fix: use combat_power field on edit 2025-06-09 18:40:06 +09:00
7463c99215 Remove duplicate signup route 2025-06-09 18:39:45 +09:00
16 changed files with 578 additions and 16 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
# Rename this file to .env and set the API base URL for your environment
# Example:
# VITE_API_BASE_URL=http://localhost:8000
VITE_API_BASE_URL=https://api.example.com

View File

@ -16,6 +16,9 @@ import Dashboard from './pages/Dashboard'
import MePage from './pages/MePage'
import CharacterEditPage from './pages/CharacterEditPage'
import GuidePage from './pages/Guide'
import FriendListPage from './pages/FriendListPage'
import FriendRequestsPage from './pages/FriendRequestsPage'
import FriendCharacterDashboard from './pages/FriendCharacterDashboard'
const darkTheme = createTheme({
palette: {
@ -46,6 +49,9 @@ function App() {
<Route path="/characters/:id/edit" element={<CharacterEditPage />} />
<Route path="/homeworks/:id/edit" element={<HomeworkEditPage />} />
<Route path="/guide" element={<GuidePage />} />
<Route path="/friends" element={<FriendListPage />} />
<Route path="/friends/:friend_id/characters" element={<FriendCharacterDashboard />} />
<Route path="/friends/requests" element={<FriendRequestsPage />} />
</Routes>
</Layout>
</Router>

View File

@ -0,0 +1,198 @@
import { useState } from 'react'
import api from '../lib/api'
interface Props {
onClose: () => void
}
export default function FriendSearchDialog({ onClose }: Props) {
const [mode, setMode] = useState<'email' | 'character'>('email')
const [input, setInput] = useState({ email: '', name: '', server: '' })
const [result, setResult] = useState<any | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const handleSearch = async () => {
setLoading(true)
setError(null)
setResult(null)
try {
const res = mode === 'email'
? await api.get('/users/public-info', { params: { email: input.email } })
: await api.get('/users/by-character', {
params: { server: input.server, name: input.name }
})
setResult(res.data)
} catch (e: any) {
setError(e.response?.data?.detail || '검색에 실패했습니다.')
} finally {
setLoading(false)
}
}
const handleRequest = async () => {
try {
await api.post('/friends/request', { to_user_email: result.email })
alert('친구 요청을 보냈습니다.')
onClose()
} catch {
alert('요청 실패')
}
}
return (
<>
<div className="popup-backdrop" onClick={onClose} />
<div className="popup">
<h3> </h3>
<div className="radio-group">
<label className="radio-option">
<input
type="radio"
name="search-mode"
checked={mode === 'email'}
onChange={() => setMode('email')}
/>
</label>
<label className="radio-option">
<input
type="radio"
name="search-mode"
checked={mode === 'character'}
onChange={() => setMode('character')}
/>
</label>
</div>
{mode === 'email' ? (
<input
type="text"
placeholder="example@naver.com"
value={input.email}
onChange={e => setInput({ ...input, email: e.target.value })}
/>
) : (
<>
<input
type="text"
placeholder="서버명"
value={input.server}
onChange={e => setInput({ ...input, server: e.target.value })}
/>
<input
type="text"
placeholder="캐릭터명"
value={input.name}
onChange={e => setInput({ ...input, name: e.target.value })}
/>
</>
)}
<div style={{ marginTop: '8px' }}>
<button onClick={handleSearch} disabled={loading}>
{loading ? '검색 중...' : '검색'}
</button>
<button onClick={onClose}></button>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
{result && (
<div style={{ marginTop: '10px' }}>
<p>: {result.email}</p>
<p> : {result.is_public ? '공개' : '비공개'}</p>
{result.is_friend ? (
<p> </p>
) : result.request_sent ? (
<p> </p>
) : result.request_received ? (
<p>📩 ( )</p>
) : (
<button onClick={handleRequest}> </button>
)}
</div>
)}
</div>
<style>{`
.popup-backdrop {
position: fixed;
top: 0; left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
}
.popup {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: #222;
color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px #000;
z-index: 1001;
min-width: 280px;
}
.popup input {
display: block;
margin-top: 8px;
width: 100%;
padding: 5px;
}
.popup button {
margin-right: 8px;
margin-top: 10px;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 6px;
margin: 10px 0;
}
.radio-option {
display: inline-flex;
align-items: center;
font-size: 14px;
color: white;
white-space: nowrap;
}
.radio-option input[type="radio"] {
margin-right: 6px;
appearance: none;
width: 16px;
height: 16px;
border: 2px solid #aaa;
border-radius: 50%;
display: inline-block;
position: relative;
cursor: pointer;
}
.radio-option input[type="radio"]:checked::before {
content: "";
display: block;
width: 8px;
height: 8px;
background-color: #00aaff;
border-radius: 50%;
position: absolute;
top: 3px;
left: 3px;
}
`}</style>
</>
)
}

View File

@ -19,6 +19,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const [anchorElCharacter, setAnchorElCharacter] = useState<null | HTMLElement>(null)
const [anchorElHomework, setAnchorElHomework] = useState<null | HTMLElement>(null)
const [anchorElFriend, setAnchorElFriend] = useState<null | HTMLElement>(null)
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
const handleMenuOpen = (
@ -71,6 +72,16 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<MenuItem component={Link} to="/homeworks" onClick={handleMenuClose(setAnchorElHomework)}> </MenuItem>
</Menu>
<Button color="inherit" onClick={handleMenuOpen(setAnchorElFriend)}></Button>
<Menu
anchorEl={anchorElFriend}
open={Boolean(anchorElFriend)}
onClose={handleMenuClose(setAnchorElFriend)}
>
<MenuItem component={Link} to="/friends" onClick={handleMenuClose(setAnchorElFriend)}></MenuItem>
<MenuItem component={Link} to="/friends/requests" onClick={handleMenuClose(setAnchorElFriend)}></MenuItem>
</Menu>
<IconButton color="inherit" onClick={handleUserMenuOpen} size="large">
<AccountCircle />
</IconButton>

View File

View File

@ -1,6 +1,5 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
// ✅ 추가: AuthProvider import

View File

@ -9,7 +9,9 @@ import {
DialogActions,
DialogContent,
DialogContentText,
DialogTitle
DialogTitle,
FormControlLabel,
Checkbox
} from '@mui/material'
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
@ -22,6 +24,7 @@ export default function CharacterEditPage() {
const [name, setName] = useState('')
const [server, setServer] = useState('')
const [combatPower, setCombatPower] = useState('')
const [isPublic, setIsPublic] = useState(false)
const [error, setError] = useState('')
const [openConfirm, setOpenConfirm] = useState(false)
@ -31,17 +34,19 @@ export default function CharacterEditPage() {
setName(res.data.name)
setServer(res.data.server || '')
setCombatPower(String(res.data.combat_power || ''))
setIsPublic(Boolean(res.data.is_public))
})
.catch(() => setError('캐릭터 정보를 불러오는 데 실패했습니다.'))
}, [id])
const handleUpdate = async () => {
try {
await api.put(`/characters/${id}`, {
name,
server,
power: Number(combatPower)
})
await api.put(`/characters/${id}`, {
name,
server,
power: Number(combatPower),
is_public: isPublic,
})
navigate('/characters')
} catch {
setError('캐릭터 수정에 실패했습니다.')
@ -82,6 +87,10 @@ export default function CharacterEditPage() {
fullWidth
type="number"
/>
<FormControlLabel
control={<Checkbox checked={isPublic} onChange={e => setIsPublic(e.target.checked)} />}
label="친구에게 노출"
/>
{error && <Typography color="error">{error}</Typography>}

View File

@ -0,0 +1,102 @@
import {
Box,
Typography,
Card,
CardContent,
Grid,
Stack,
Checkbox,
Button
} from '@mui/material'
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import api from '../lib/api'
interface Character {
id: number
name: string
server: string
}
interface Homework {
id: number
title: string
reset_type: string
clear_count: number
complete_cnt: number
}
export default function FriendCharacterDashboard() {
const { friend_id } = useParams()
const [characters, setCharacters] = useState<Character[]>([])
const [homeworks, setHomeworks] = useState<Record<number, Homework[]>>({})
const navigate = useNavigate()
useEffect(() => {
const fetchData = async () => {
try {
const res = await api.get(`/friends/${friend_id}/characters`)
setCharacters(res.data)
const hwResults = await Promise.all(
res.data.map((char: Character) =>
api
.get(`/friends/${friend_id}/characters/${char.id}/homeworks`)
.then(r => ({ id: char.id, data: r.data }))
)
)
const map: Record<number, Homework[]> = {}
hwResults.forEach(item => {
map[item.id] = item.data
})
setHomeworks(map)
} catch (err) {
console.error('친구 대시보드 데이터를 불러오는 데 실패했습니다.', err)
}
}
fetchData()
}, [friend_id])
return (
<Box sx={{ p: 4 }}>
<Button variant="outlined" onClick={() => navigate(-1)} sx={{ mb: 2 }}>
</Button>
<Typography variant="h5" gutterBottom>
</Typography>
<Grid container spacing={2}>
{characters.map(char => (
<Grid item xs={12} sm={6} md={4} key={char.id} {...({} as any)}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{char.server} : {char.name}
</Typography>
<Stack spacing={1}>
{(homeworks[char.id] || []).map(hw => (
<Box key={hw.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
size="small"
/>
))}
</Stack>
</Box>
))}
</Stack>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
)
}

View File

@ -0,0 +1,85 @@
import {
Box,
Card,
CardContent,
Container,
Grid,
Typography,
Button,
} from '@mui/material'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../lib/api'
import FriendSearchDialog from '../components/FriendSearchDialog'
interface Friend {
id: number
email: string
}
export default function FriendListPage() {
const [friends, setFriends] = useState<Friend[]>([])
const [showDialog, setShowDialog] = useState(false)
const navigate = useNavigate()
useEffect(() => {
const fetchFriends = async () => {
try {
const res = await api.get('/friends/list')
setFriends(res.data)
} catch (e) {
console.error('친구 목록 불러오기 실패', e)
}
}
fetchFriends()
}, [])
return (
<Container sx={{ mt: 4 }}>
<Typography variant="h5" gutterBottom>
</Typography>
<Grid container spacing={2}>
{friends.map(friend => (
<Grid item xs={12} sm={6} md={4} key={friend.id} {...({} as any)}>
<Card>
<CardContent>
<Typography variant="h6">{friend.email}</Typography>
<Box sx={{ mt: 1 }}>
<Button
variant="outlined"
onClick={() =>
navigate(`/friends/${friend.id}/characters`)
}
>
</Button>
</Box>
</CardContent>
</Card>
</Grid>
))}
<Grid item xs={12} sm={6} md={4} {...({} as any)}>
<Card
onClick={() => setShowDialog(true)}
sx={{
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
}}
>
<CardContent>
<Typography variant="h6" align="center">
+
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{showDialog && <FriendSearchDialog onClose={() => setShowDialog(false)} />}
</Container>
)
}

View File

@ -0,0 +1,106 @@
import { useEffect, useState } from 'react'
import api from '../lib/api'
interface FriendRequest {
id: number
from_user_id: number
to_user_id: number
from_user_email?: string
to_user_email?: string
status: 'pending' | 'accepted' | 'rejected' | 'cancelled'
created_at: string
}
export default function FriendRequestsPage() {
const [tab, setTab] = useState<'received' | 'sent'>('received')
const [requests, setRequests] = useState<FriendRequest[]>([])
const [emailMap, setEmailMap] = useState<Record<number, string>>({})
useEffect(() => {
const fetchRequests = async () => {
const url =
tab === 'received' ? '/friends/requests/received' : '/friends/requests/sent'
const res = await api.get(url)
setRequests(res.data)
const emails = await Promise.all(
res.data.map(async (r: FriendRequest) => {
const targetId = tab === 'received' ? r.from_user_id : r.to_user_id
const emailFromResponse =
tab === 'received' ? r.from_user_email : r.to_user_email
if (emailFromResponse) {
return [targetId, emailFromResponse] as [number, string]
}
const user = await api.get(`/users/${targetId}`)
return [targetId, user.data.email] as [number, string]
})
)
setEmailMap(Object.fromEntries(emails))
}
fetchRequests()
}, [tab])
const handleRespond = async (id: number, accept: boolean) => {
await api.post(`/friends/requests/${id}/respond`, null, { params: { accept } })
alert(accept ? '친구 요청을 수락했습니다.' : '친구 요청을 거절했습니다.')
const req = requests.find(r => r.id === id)
const targetId = req ? (tab === 'received' ? req.from_user_id : req.to_user_id) : null
setRequests(prev => prev.filter(r => r.id !== id))
if (targetId !== null) {
setEmailMap(prev => {
const newMap = { ...prev }
delete newMap[targetId]
return newMap
})
}
}
const handleCancel = async (id: number) => {
await api.post(`/friends/requests/${id}/cancel`)
alert('요청을 취소했습니다.')
const req = requests.find(r => r.id === id)
const targetId = req ? (tab === 'received' ? req.from_user_id : req.to_user_id) : null
setRequests(prev => prev.filter(r => r.id !== id))
if (targetId !== null) {
setEmailMap(prev => {
const newMap = { ...prev }
delete newMap[targetId]
return newMap
})
}
}
return (
<div>
<h2> </h2>
<div>
<button onClick={() => setTab('received')}> </button>
<button onClick={() => setTab('sent')}> </button>
</div>
{requests.length === 0 ? (
<p> .</p>
) : (
<ul>
{requests.map(req => {
const targetId = tab === 'received' ? req.from_user_id : req.to_user_id
const email = emailMap[targetId] || '로딩중...'
return (
<li key={req.id}>
{email}{' '}
{tab === 'received' ? (
<>
<button onClick={() => handleRespond(req.id, true)}></button>
<button onClick={() => handleRespond(req.id, false)}></button>
</>
) : (
<button onClick={() => handleCancel(req.id)}></button>
)}
</li>
)
})}
</ul>
)}
</div>
)
}

View File

@ -3,7 +3,7 @@ import { Box, Container, Typography, Divider, List, ListItem, ListItemText } fro
const GuidePage = () => {
return (
<Container maxWidth="md" sx={{ py: 4 }}>
<Typography variant="h4" gutterBottom> (v1.0)</Typography>
<Typography variant="h4" gutterBottom> (v2.0)</Typography>
<Typography variant="subtitle1" gutterBottom>
.
</Typography>
@ -42,10 +42,18 @@ const GuidePage = () => {
<ListItem><ListItemText primary="사용자는 별도 조작 없이 다음 날 다시 체크 가능" /></ListItem>
</List>
<Typography variant="h6" sx={{ mt: 3 }}>6. </Typography>
<List dense>
<ListItem><ListItemText primary="상단 메뉴 '친구'에서 친구목록 확인 및 검색 추가" /></ListItem>
<ListItem><ListItemText primary="요청관리에서 받은 요청을 수락/거절하고 보낸 요청을 취소" /></ListItem>
<ListItem><ListItemText primary="친구의 캐릭터 공개 숙제를 볼 수 있으나 수정은 불가" /></ListItem>
<ListItem><ListItemText primary="캐릭터/숙제 등록 시 '친구에게 노출' 체크박스로 공개 여부 설정" /></ListItem>
</List>
<Divider sx={{ my: 3 }} />
<Typography variant="caption" color="text.secondary">
© . v1.0 .
© . v2.0 .
</Typography>
</Container>
);

View File

@ -6,6 +6,7 @@ export default function Home() {
const { isLoggedIn } = useAuth()
const updates = [
{ date: '2025-06-11', version: 'v2.0', content: '친구 기능 추가: 친구목록/요청 관리 및 캐릭터 공개 보기 지원' },
{ date: '2025-06-06', version: 'v1.1', content: '피드백 및 수정요청 및 기능개선은 nightbug@naver.com 으로 부탁드립니다.' },
{ date: '2025-05-28', version: 'v1.1', content: '캐릭터 및 숙제 목록에서 순서변경기능 추가' },
{ date: '2025-05-28', version: 'v1.1', content: '홈화면에 업데이트 내역 노출' },

View File

@ -1,6 +1,7 @@
import {
Box, Button, Container, Paper, TextField, Typography, MenuItem,
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions,
FormControlLabel, Checkbox
} from '@mui/material'
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
@ -14,6 +15,7 @@ export default function HomeworkEditPage() {
const [description, setDescription] = useState('')
const [resetType, setResetType] = useState('')
const [clearCount, setClearCount] = useState('')
const [isPublic, setIsPublic] = useState(false)
const [error, setError] = useState('')
const [openConfirm, setOpenConfirm] = useState(false)
@ -25,6 +27,7 @@ export default function HomeworkEditPage() {
setDescription(hw.description || '')
setResetType(hw.reset_type)
setClearCount(String(hw.clear_count || ''))
setIsPublic(Boolean(hw.is_public))
})
.catch(() => setError('숙제 정보를 불러오는 데 실패했습니다.'))
}, [id])
@ -32,10 +35,11 @@ export default function HomeworkEditPage() {
const handleUpdate = async () => {
try {
await api.put(`/homeworks/${id}`, {
name: title, // ✅ title → name
title,
description,
repeat_type: resetType, // ✅ reset_type → repeat_type
repeat_count: Number(clearCount) // ✅ clear_count → repeat_count
reset_type: resetType,
clear_count: Number(clearCount),
is_public: isPublic,
})
navigate('/homeworks')
} catch {
@ -89,6 +93,10 @@ export default function HomeworkEditPage() {
type="number"
fullWidth
/>
<FormControlLabel
control={<Checkbox checked={isPublic} onChange={e => setIsPublic(e.target.checked)} />}
label="친구에게 노출"
/>
{error && <Typography color="error">{error}</Typography>}

View File

@ -20,7 +20,7 @@ export default function Login() {
password,
})
const { access_token, token_type } = res.data
const { access_token } = res.data
console.log('로그인 성공:', access_token)
login(access_token) // ✅ 전역 상태 + localStorage 동시 반영

View File

@ -1,4 +1,4 @@
import { Box, Button, Container, Paper, TextField, Typography } from '@mui/material'
import { Box, Button, Container, Paper, TextField, Typography, FormControlLabel, Checkbox } from '@mui/material'
import { useState } from 'react'
import api from '../lib/api'
import { useNavigate } from 'react-router-dom'
@ -8,6 +8,7 @@ export default function RegisterCharacter() {
const [server, setServer] = useState('')
const [job, setJob] = useState('')
const [power, setPower] = useState('')
const [isPublic, setIsPublic] = useState(false)
const navigate = useNavigate()
const handleSubmit = async () => {
@ -22,6 +23,7 @@ export default function RegisterCharacter() {
server: server || undefined,
job: job || undefined,
combat_power: power ? parseInt(power, 10) : undefined,
is_public: isPublic,
})
alert('캐릭터가 성공적으로 등록되었습니다.')
navigate('/characters')
@ -64,6 +66,10 @@ export default function RegisterCharacter() {
onChange={(e) => setPower(e.target.value)}
fullWidth
/>
<FormControlLabel
control={<Checkbox checked={isPublic} onChange={e => setIsPublic(e.target.checked)} />}
label="친구에게 노출"
/>
<Button variant="contained" onClick={handleSubmit}>
</Button>

View File

@ -4,7 +4,9 @@ import {
Button,
Typography,
MenuItem,
Container
Container,
FormControlLabel,
Checkbox
} from '@mui/material'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
@ -15,6 +17,8 @@ export default function RegisterHomework() {
const [description, setDescription] = useState('')
const [resetType, setResetType] = useState('')
const [clearCount, setClearCount] = useState(0)
const [resetTime, setResetTime] = useState('')
const [isPublic, setIsPublic] = useState(false)
const [error, setError] = useState('')
const navigate = useNavigate()
@ -25,7 +29,9 @@ export default function RegisterHomework() {
title,
description,
reset_type: resetType,
reset_time: resetTime,
clear_count: clearCount,
is_public: isPublic,
})
navigate('/homeworks')
} catch (err) {
@ -65,6 +71,14 @@ export default function RegisterHomework() {
<MenuItem value="weekly"></MenuItem>
<MenuItem value="monthly"></MenuItem>
</TextField>
<TextField
fullWidth
type="time"
label="초기화 시간"
margin="normal"
value={resetTime}
onChange={(e) => setResetTime(e.target.value)}
/>
<TextField
fullWidth
type="number"
@ -74,6 +88,10 @@ export default function RegisterHomework() {
value={clearCount}
onChange={(e) => setClearCount(parseInt(e.target.value) || 0)}
/>
<FormControlLabel
control={<Checkbox checked={isPublic} onChange={e => setIsPublic(e.target.checked)} />}
label="친구에게 노출"
/>
{error && <Typography color="error">{error}</Typography>}
<Button fullWidth variant="contained" sx={{ mt: 2 }} onClick={handleSubmit}>