친구 작업중

This commit is contained in:
SR07 2025-06-09 18:52:13 +09:00
parent 2166685131
commit 273b76ea77
5 changed files with 343 additions and 2 deletions

View File

@ -16,6 +16,8 @@ import Dashboard from './pages/Dashboard'
import MePage from './pages/MePage' import MePage from './pages/MePage'
import CharacterEditPage from './pages/CharacterEditPage' import CharacterEditPage from './pages/CharacterEditPage'
import GuidePage from './pages/Guide' import GuidePage from './pages/Guide'
import FriendListPage from './pages/FriendListPage'
import FriendRequestsPage from './pages/FriendRequestsPage'
const darkTheme = createTheme({ const darkTheme = createTheme({
palette: { palette: {
@ -46,6 +48,10 @@ function App() {
<Route path="/characters/:id/edit" element={<CharacterEditPage />} /> <Route path="/characters/:id/edit" element={<CharacterEditPage />} />
<Route path="/homeworks/:id/edit" element={<HomeworkEditPage />} /> <Route path="/homeworks/:id/edit" element={<HomeworkEditPage />} />
<Route path="/guide" element={<GuidePage />} /> <Route path="/guide" element={<GuidePage />} />
<Route path="/friends" element={<FriendListPage />} />
<Route path="/friends/requests" element={<FriendRequestsPage />} />
{/*<Route path="/friends/:friendId/characters" element={<FriendCharacterPage />} />*/}
{/*<Route path="/friends/:friendId/characters/:characterId/homeworks" element={<FriendHomeworkPage />} />*/}
</Routes> </Routes>
</Layout> </Layout>
</Router> </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

@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
const api = axios.create({ const api = axios.create({
baseURL: 'https://api.biryu2000.kr', // baseURL: 'https://api.biryu2000.kr',
// baseURL: 'http://localhost:8000', baseURL: 'http://localhost:8000',
}) })
// 요청 시 토큰 자동 추가 // 요청 시 토큰 자동 추가

View File

@ -0,0 +1,57 @@
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 [friendIds, setFriendIds] = useState<number[]>([])
const [friends, setFriends] = useState<Friend[]>([])
const [showDialog, setShowDialog] = useState(false)
const navigate = useNavigate()
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)
} catch (e) {
console.error('친구 목록 불러오기 실패', e)
}
}
fetchFriends()
}, [])
return (
<div>
<h2> </h2>
<button onClick={() => setShowDialog(true)}>+ </button>
{friends.length === 0 ? (
<p> .</p>
) : (
<ul>
{friends.map(friend => (
<li key={friend.id}>
{friend.email}
<button onClick={() => navigate(`/friends/${friend.id}/characters`)}>
</button>
</li>
))}
</ul>
)}
{showDialog && <FriendSearchDialog onClose={() => setShowDialog(false)} />}
</div>
)
}

View File

@ -0,0 +1,80 @@
import { useEffect, useState } from 'react'
import api from '../lib/api'
interface FriendRequest {
id: number
from_user_id: number
to_user_id: number
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 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]))
)
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 ? '친구 요청을 수락했습니다.' : '친구 요청을 거절했습니다.')
setRequests(requests.filter(r => r.id !== id))
}
const handleCancel = async (id: number) => {
await api.post(`/friends/requests/${id}/cancel`)
alert('요청을 취소했습니다.')
setRequests(requests.filter(r => r.id !== id))
}
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>
)
}