From 273b76ea774496542320868adabcb9ad63f405d8 Mon Sep 17 00:00:00 2001 From: SR07 Date: Mon, 9 Jun 2025 18:52:13 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=9C=EA=B5=AC=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 6 + src/components/FriendSearchDialog.tsx | 198 ++++++++++++++++++++++++++ src/lib/api.ts | 4 +- src/pages/FriendListPage.tsx | 57 ++++++++ src/pages/FriendRequestsPage.tsx | 80 +++++++++++ 5 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 src/components/FriendSearchDialog.tsx create mode 100644 src/pages/FriendListPage.tsx create mode 100644 src/pages/FriendRequestsPage.tsx diff --git a/src/App.tsx b/src/App.tsx index fa6ee64..8d3e8a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,8 @@ 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' const darkTheme = createTheme({ palette: { @@ -46,6 +48,10 @@ function App() { } /> } /> } /> + } /> + } /> + {/*} />*/} + {/*} />*/} diff --git a/src/components/FriendSearchDialog.tsx b/src/components/FriendSearchDialog.tsx new file mode 100644 index 0000000..1396972 --- /dev/null +++ b/src/components/FriendSearchDialog.tsx @@ -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(null) + const [error, setError] = useState(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 ( + <> +
+
+

친구 추가

+ +
+ + +
+ + + {mode === 'email' ? ( + setInput({ ...input, email: e.target.value })} + /> + ) : ( + <> + setInput({ ...input, server: e.target.value })} + /> + setInput({ ...input, name: e.target.value })} + /> + + )} + +
+ + +
+ + {error &&

{error}

} + + {result && ( +
+

이메일: {result.email}

+

공개 여부: {result.is_public ? '공개' : '비공개'}

+ + {result.is_friend ? ( +

✅ 이미 친구입니다

+ ) : result.request_sent ? ( +

⏳ 이미 요청 보낸 상태입니다

+ ) : result.request_received ? ( +

📩 상대가 요청을 보냈습니다 (요청 수락 화면에서 처리)

+ ) : ( + + )} +
+ )} +
+ + + + ) +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 9c788fe..7f420d7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,8 +1,8 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'https://api.biryu2000.kr', - // baseURL: 'http://localhost:8000', + // baseURL: 'https://api.biryu2000.kr', + baseURL: 'http://localhost:8000', }) // 요청 시 토큰 자동 추가 diff --git a/src/pages/FriendListPage.tsx b/src/pages/FriendListPage.tsx new file mode 100644 index 0000000..f8dd699 --- /dev/null +++ b/src/pages/FriendListPage.tsx @@ -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([]) + const [friends, setFriends] = useState([]) + 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 ( +
+

친구 목록

+ + + {friends.length === 0 ? ( +

친구가 없습니다.

+ ) : ( +
    + {friends.map(friend => ( +
  • + {friend.email} + +
  • + ))} +
+ )} + + {showDialog && setShowDialog(false)} />} +
+ ) +} diff --git a/src/pages/FriendRequestsPage.tsx b/src/pages/FriendRequestsPage.tsx new file mode 100644 index 0000000..651b7f7 --- /dev/null +++ b/src/pages/FriendRequestsPage.tsx @@ -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([]) + const [emailMap, setEmailMap] = useState>({}) + + 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 ( +
+

친구 요청 관리

+
+ + +
+ + {requests.length === 0 ? ( +

요청이 없습니다.

+ ) : ( +
    + {requests.map(req => { + const targetId = tab === 'received' ? req.from_user_id : req.to_user_id + const email = emailMap[targetId] || '로딩중...' + return ( +
  • + {email}{' '} + {tab === 'received' ? ( + <> + + + + ) : ( + + )} +
  • + ) + })} +
+ )} +
+ ) +}