Style friend list with cards
This commit is contained in:
parent
dbfc2faaa9
commit
bfd04fc68c
@ -18,6 +18,7 @@ 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: {
|
||||
@ -49,6 +50,7 @@ function App() {
|
||||
<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>
|
||||
|
||||
@ -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,6 +34,7 @@ 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])
|
||||
@ -40,7 +44,8 @@ export default function CharacterEditPage() {
|
||||
await api.put(`/characters/${id}`, {
|
||||
name,
|
||||
server,
|
||||
combat_power: Number(combatPower)
|
||||
power: Number(combatPower),
|
||||
is_public: isPublic,
|
||||
})
|
||||
navigate('/characters')
|
||||
} catch {
|
||||
@ -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>}
|
||||
|
||||
|
||||
102
src/pages/FriendCharacterDashboard.tsx
Normal file
102
src/pages/FriendCharacterDashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
|
||||
export default function FriendList() {
|
||||
return (
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
친구 목록
|
||||
</Typography>
|
||||
<Typography>준비 중...</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -1,3 +1,12 @@
|
||||
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'
|
||||
@ -9,7 +18,6 @@ interface Friend {
|
||||
}
|
||||
|
||||
export default function FriendListPage() {
|
||||
const [friendIds, setFriendIds] = useState<number[]>([])
|
||||
const [friends, setFriends] = useState<Friend[]>([])
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
@ -17,13 +25,8 @@ export default function FriendListPage() {
|
||||
useEffect(() => {
|
||||
const fetchFriends = async () => {
|
||||
try {
|
||||
const ids: number[] = await api.get('/friends/list').then(res => res.data)
|
||||
setFriendIds(ids)
|
||||
|
||||
const friendInfos = await Promise.all(
|
||||
ids.map(id => api.get(`/users/${id}`).then(res => res.data))
|
||||
)
|
||||
setFriends(friendInfos)
|
||||
const res = await api.get('/friends/list')
|
||||
setFriends(res.data)
|
||||
} catch (e) {
|
||||
console.error('친구 목록 불러오기 실패', e)
|
||||
}
|
||||
@ -32,26 +35,51 @@ export default function FriendListPage() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>친구 목록</h2>
|
||||
<button onClick={() => setShowDialog(true)}>+ 친구 추가</button>
|
||||
|
||||
{friends.length === 0 ? (
|
||||
<p>친구가 없습니다.</p>
|
||||
) : (
|
||||
<ul>
|
||||
<Container sx={{ mt: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
친구 목록
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{friends.map(friend => (
|
||||
<li key={friend.id}>
|
||||
{friend.email}
|
||||
<button onClick={() => navigate(`/friends/${friend.id}/characters`)}>
|
||||
<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>
|
||||
</li>
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<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)} />}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
|
||||
export default function FriendRequests() {
|
||||
return (
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
요청 관리
|
||||
</Typography>
|
||||
<Typography>준비 중...</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -5,6 +5,8 @@ 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
|
||||
}
|
||||
@ -21,11 +23,17 @@ export default function FriendRequestsPage() {
|
||||
const res = await api.get(url)
|
||||
setRequests(res.data)
|
||||
|
||||
const userIds = res.data.map((r: FriendRequest) =>
|
||||
tab === 'received' ? r.from_user_id : r.to_user_id
|
||||
)
|
||||
const emails = await Promise.all(
|
||||
userIds.map(id => api.get(`/users/${id}`).then(res => [id, res.data.email]))
|
||||
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))
|
||||
}
|
||||
@ -35,13 +43,31 @@ export default function FriendRequestsPage() {
|
||||
const handleRespond = async (id: number, accept: boolean) => {
|
||||
await api.post(`/friends/requests/${id}/respond`, null, { params: { accept } })
|
||||
alert(accept ? '친구 요청을 수락했습니다.' : '친구 요청을 거절했습니다.')
|
||||
setRequests(requests.filter(r => r.id !== id))
|
||||
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('요청을 취소했습니다.')
|
||||
setRequests(requests.filter(r => r.id !== id))
|
||||
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 (
|
||||
|
||||
@ -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>}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}>
|
||||
등록하기
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user