친구 작업중
This commit is contained in:
parent
2166685131
commit
273b76ea77
@ -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() {
|
||||
<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/requests" element={<FriendRequestsPage />} />
|
||||
{/*<Route path="/friends/:friendId/characters" element={<FriendCharacterPage />} />*/}
|
||||
{/*<Route path="/friends/:friendId/characters/:characterId/homeworks" element={<FriendHomeworkPage />} />*/}
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Router>
|
||||
|
||||
198
src/components/FriendSearchDialog.tsx
Normal file
198
src/components/FriendSearchDialog.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
})
|
||||
|
||||
// 요청 시 토큰 자동 추가
|
||||
|
||||
57
src/pages/FriendListPage.tsx
Normal file
57
src/pages/FriendListPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
80
src/pages/FriendRequestsPage.tsx
Normal file
80
src/pages/FriendRequestsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user