refactor: 프로필/관리자 페이지 UI 개선
- 모바일 툴바 h-14 통일 - 데스크탑 사이드바 레이아웃 통합 - 관리자 페이지 프로필과 동일한 스타일 적용 - 불필요한 hooks/api.js 파일 제거 - body 배경 gradient에서 단색으로 변경
This commit is contained in:
parent
9adc0fe19b
commit
ba907ec8eb
5 changed files with 186 additions and 140 deletions
|
|
@ -34,8 +34,8 @@ function App() {
|
|||
const isAuthPage = ['/login', '/register'].includes(location.pathname) ||
|
||||
location.pathname.startsWith('/verify/');
|
||||
|
||||
// 별도 레이아웃 페이지 (관리자, 프로필)
|
||||
const isStandalonePage = ['/admin', '/profile'].includes(location.pathname);
|
||||
// 별도 레이아웃 페이지 (모바일에서만 standalone)
|
||||
const isStandalonePage = (location.pathname === '/admin' || location.pathname === '/profile') && isMobile;
|
||||
|
||||
// 라우트 전환 시 스크롤 맨 위로
|
||||
useEffect(() => {
|
||||
|
|
@ -68,8 +68,8 @@ function App() {
|
|||
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route path="/admin" element={<PageWrapper><Admin /></PageWrapper>} />
|
||||
<Route path="/profile" element={<PageWrapper><ProfilePage /></PageWrapper>} />
|
||||
<Route path="/admin" element={<PageWrapper><Admin isMobile={isMobile} /></PageWrapper>} />
|
||||
<Route path="/profile" element={<PageWrapper><ProfilePage isMobile={isMobile} /></PageWrapper>} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
|
@ -95,6 +95,8 @@ function App() {
|
|||
<Route path="/players" element={<PageWrapper><PlayersPage isMobile={isMobile} /></PageWrapper>} />
|
||||
<Route path="/player/:uuid/stats" element={<PageWrapper><PlayerStatsPage isMobile={isMobile} /></PageWrapper>} />
|
||||
<Route path="/worldmap" element={<PageWrapper><WorldMapPage isMobile={isMobile} /></PageWrapper>} />
|
||||
<Route path="/profile" element={<PageWrapper><ProfilePage isMobile={isMobile} /></PageWrapper>} />
|
||||
<Route path="/admin" element={<PageWrapper><Admin isMobile={isMobile} /></PageWrapper>} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
setMinecraftLink(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('연동 상태 확인 실패:', error);
|
||||
// 에러 무시
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -72,10 +72,20 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
// 서버 상태 확인 (socket.io) + 닉네임 변경 시에만 동기화
|
||||
useEffect(() => {
|
||||
const socket = io(window.location.origin, { path: '/socket.io' });
|
||||
let isSyncing = false;
|
||||
|
||||
// 닉네임 동기화 함수 (변경 시에만 호출됨)
|
||||
const syncNickname = async () => {
|
||||
if (!isLoggedIn) return;
|
||||
socket.on('status', async (status) => {
|
||||
setServerOnline(status?.online || false);
|
||||
|
||||
// 동기화 중이면 스킵
|
||||
if (isSyncing) return;
|
||||
|
||||
// 연동된 유저인 경우, 소켓에서 받은 닉네임과 현재 닉네임 비교
|
||||
if (status?.online && minecraftLink?.minecraftUuid && status?.players?.list) {
|
||||
const playerInGame = status.players.list.find(p => p.uuid === minecraftLink.minecraftUuid);
|
||||
// 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
|
||||
if (playerInGame && playerInGame.name !== user?.name) {
|
||||
isSyncing = true;
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch('/link/status', {
|
||||
|
|
@ -84,29 +94,19 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
const data = await res.json();
|
||||
if (data.linked) {
|
||||
setMinecraftLink(data);
|
||||
// fetch 완료 후 checkAuth로 user.name 갱신
|
||||
await checkAuth();
|
||||
}
|
||||
} catch (error) {
|
||||
// 무시
|
||||
} finally {
|
||||
isSyncing = false;
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('status', (status) => {
|
||||
setServerOnline(status?.online || false);
|
||||
|
||||
// 연동된 유저인 경우, 소켓에서 받은 닉네임과 현재 닉네임 비교
|
||||
if (status?.online && minecraftLink?.minecraftUuid && status?.players?.list) {
|
||||
const playerInGame = status.players.list.find(p => p.uuid === minecraftLink.minecraftUuid);
|
||||
// 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
|
||||
if (playerInGame && playerInGame.name !== user?.name) {
|
||||
syncNickname();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => socket.disconnect();
|
||||
}, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name]);
|
||||
}, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name, checkAuth]);
|
||||
|
||||
// 토스트 자동 숨기기
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 기본 body 스타일 - 다크 배경 (약간 밝게) */
|
||||
/* 기본 body 스타일 - 다크 배경 */
|
||||
body {
|
||||
background: linear-gradient(135deg, #141414 0%, #181a18 50%, #141414 100%);
|
||||
background-attachment: fixed;
|
||||
background: #141414;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useNavigate, useLocation, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Shield, LogOut, Settings, Server, Users, Loader2 } from 'lucide-react';
|
||||
import { Shield, ArrowLeft, Settings, Server, Users, Loader2, User, Terminal, MessageSquare } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
export default function Admin() {
|
||||
const { isLoggedIn, isAdmin, user, loading, logout } = useAuth();
|
||||
export default function Admin({ isMobile = false }) {
|
||||
const { isLoggedIn, isAdmin, user, loading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [toast, setToast] = useState(null);
|
||||
|
|
@ -18,7 +18,6 @@ export default function Admin() {
|
|||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!isLoggedIn) {
|
||||
// 로그인 후 다시 admin으로 돌아올 수 있도록 현재 경로 전달
|
||||
navigate('/login', { state: { from: location.pathname } });
|
||||
} else if (!isAdmin) {
|
||||
setToast('관리자 권한이 필요합니다.');
|
||||
|
|
@ -29,11 +28,6 @@ export default function Admin() {
|
|||
}
|
||||
}, [isLoggedIn, isAdmin, loading, navigate, location.pathname]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
// 토스트 자동 숨기기
|
||||
useEffect(() => {
|
||||
if (toast) {
|
||||
|
|
@ -74,87 +68,128 @@ export default function Admin() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 rounded-xl bg-yellow-500/20 border border-yellow-500/30">
|
||||
<Shield className="w-6 h-6 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">관리자 페이지</h1>
|
||||
<p className="text-sm text-zinc-400">서버 관리 및 설정</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition-colors"
|
||||
<div className="pb-8">
|
||||
{/* 모바일용 헤더 */}
|
||||
{isMobile && (
|
||||
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="p-2 -ml-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
로그아웃
|
||||
</button>
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<div className="ml-2">
|
||||
<h1 className="text-lg font-bold text-white">관리자</h1>
|
||||
<p className="text-xs text-zinc-500">서버 관리 및 설정</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<main className={`max-w-4xl mx-auto px-4 sm:px-6 ${isMobile ? 'py-4' : 'py-8'} space-y-4 sm:space-y-6`}>
|
||||
{/* 데스크탑용 타이틀 */}
|
||||
{!isMobile && (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">관리자 페이지</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">서버 관리 및 설정</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 관리자 정보 카드 */}
|
||||
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<Shield size={20} className="text-yellow-500" />
|
||||
관리자 정보
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-yellow-500/20 border border-yellow-500/30 flex items-center justify-center">
|
||||
<User size={32} className="text-yellow-500" />
|
||||
</div>
|
||||
|
||||
{/* 사용자 정보 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">로그인 정보</h2>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div>
|
||||
<span className="text-zinc-500">이름</span>
|
||||
<p className="text-white mt-1">{user?.name || '-'}</p>
|
||||
<p className="text-sm text-zinc-500">닉네임</p>
|
||||
<p className="text-lg font-medium text-white">{user?.name || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500">이메일</span>
|
||||
<p className="text-white mt-1">{user?.email}</p>
|
||||
<p className="text-sm text-zinc-500">이메일</p>
|
||||
<p className="text-white">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 관리 기능 카드 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 서버 상태 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Server className="w-5 h-5 text-mc-green" />
|
||||
<h2 className="text-lg font-semibold text-white">서버 상태</h2>
|
||||
{/* 서버 관리 섹션 */}
|
||||
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<Server size={20} className="text-mc-green" />
|
||||
서버 관리
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 서버 명령어 */}
|
||||
<div className="p-4 bg-zinc-800/50 rounded-xl">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Terminal size={18} className="text-zinc-400" />
|
||||
<p className="font-medium text-white">서버 명령어</p>
|
||||
</div>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
마인크래프트 서버 상태 모니터링 및 관리
|
||||
<p className="text-sm text-zinc-400 mb-3">
|
||||
마인크래프트 서버에 명령어를 전송합니다.
|
||||
</p>
|
||||
<div className="mt-4 py-2 px-3 bg-mc-green/10 border border-mc-green/20 rounded-lg text-mc-green text-sm inline-block">
|
||||
정상 작동 중
|
||||
<div className="text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 플레이어 관리 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Users className="w-5 h-5 text-blue-400" />
|
||||
<h2 className="text-lg font-semibold text-white">플레이어 관리</h2>
|
||||
{/* 공지 전송 */}
|
||||
<div className="p-4 bg-zinc-800/50 rounded-xl">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<MessageSquare size={18} className="text-zinc-400" />
|
||||
<p className="font-medium text-white">공지 전송</p>
|
||||
</div>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
<p className="text-sm text-zinc-400 mb-3">
|
||||
게임 내 모든 플레이어에게 공지를 전송합니다.
|
||||
</p>
|
||||
<div className="text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 플레이어 관리 섹션 */}
|
||||
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<Users size={20} className="text-blue-400" />
|
||||
플레이어 관리
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-zinc-400">
|
||||
접속 중인 플레이어 목록 및 관리 기능
|
||||
</p>
|
||||
<div className="mt-4 text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 설정 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 md:col-span-2">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Settings className="w-5 h-5 text-zinc-400" />
|
||||
<h2 className="text-lg font-semibold text-white">설정</h2>
|
||||
</div>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
{/* 설정 섹션 */}
|
||||
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<Settings size={20} className="text-zinc-400" />
|
||||
설정
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-zinc-400">
|
||||
대시보드 설정 및 구성 관리
|
||||
</p>
|
||||
<div className="mt-4 text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { ArrowLeft, Copy, Check, Gamepad2, Link as LinkIcon, Unlink, RefreshCw,
|
|||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
export default function ProfilePage() {
|
||||
export default function ProfilePage({ isMobile = false }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, isLoggedIn, loading, checkAuth, logout } = useAuth();
|
||||
|
|
@ -32,6 +32,20 @@ export default function ProfilePage() {
|
|||
}, [loading, isLoggedIn, navigate, location.pathname]);
|
||||
|
||||
// 연동 상태 확인
|
||||
const fetchLinkStatus = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch('/link/status', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const data = await res.json();
|
||||
setLinkStatus(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLinkStatus();
|
||||
}, []);
|
||||
|
|
@ -46,7 +60,6 @@ export default function ProfilePage() {
|
|||
setPolling(false);
|
||||
setLinkToken(null);
|
||||
setCommand('');
|
||||
// 유저 정보 새로고침
|
||||
await checkAuth();
|
||||
}
|
||||
}, 3000);
|
||||
|
|
@ -59,10 +72,8 @@ export default function ProfilePage() {
|
|||
if (!linkToken) return;
|
||||
|
||||
const handleBeforeUnload = (e) => {
|
||||
// 토큰 무효화 API 호출 (sendBeacon으로 페이지 이탈 전 전송)
|
||||
const token = localStorage.getItem('token');
|
||||
navigator.sendBeacon('/link/cancel', JSON.stringify({ authToken: token }));
|
||||
|
||||
e.preventDefault();
|
||||
e.returnValue = '연동이 진행 중입니다. 페이지를 떠나시겠습니까?';
|
||||
return e.returnValue;
|
||||
|
|
@ -77,15 +88,22 @@ export default function ProfilePage() {
|
|||
if (!linkStatus?.minecraftUuid) return;
|
||||
|
||||
const socket = io(window.location.origin, { path: '/socket.io' });
|
||||
let isSyncing = false;
|
||||
|
||||
socket.on('status', async (status) => {
|
||||
if (status?.online && status?.players?.list) {
|
||||
if (!status?.online || !status?.players?.list) return;
|
||||
if (isSyncing) return;
|
||||
|
||||
const playerInGame = status.players.list.find(p => p.uuid === linkStatus.minecraftUuid);
|
||||
// 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
|
||||
if (playerInGame && playerInGame.name !== user?.name) {
|
||||
// fetchLinkStatus가 DB 업데이트 → 완료 후 checkAuth로 user.name 갱신
|
||||
isSyncing = true;
|
||||
try {
|
||||
await fetchLinkStatus();
|
||||
await checkAuth();
|
||||
} catch (error) {
|
||||
// 무시
|
||||
} finally {
|
||||
isSyncing = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -93,21 +111,6 @@ export default function ProfilePage() {
|
|||
return () => socket.disconnect();
|
||||
}, [linkStatus?.minecraftUuid, user?.name]);
|
||||
|
||||
const fetchLinkStatus = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch('/link/status', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const data = await res.json();
|
||||
setLinkStatus(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('연동 상태 확인 실패:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestLink = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
|
|
@ -175,26 +178,33 @@ export default function ProfilePage() {
|
|||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-mc-bg pb-8">
|
||||
{/* 헤더 */}
|
||||
<div className="pb-8">
|
||||
{/* 모바일용 헤더 */}
|
||||
{isMobile && (
|
||||
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||
className="p-2 -ml-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">프로필</h1>
|
||||
<p className="text-sm text-zinc-500">계정 정보 및 마인크래프트 연동</p>
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<h1 className="text-lg font-bold text-white">프로필</h1>
|
||||
<p className="text-xs text-zinc-500">계정 정보 및 마인크래프트 연동</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 py-6 sm:py-8 space-y-4 sm:space-y-6">
|
||||
<main className={`max-w-4xl mx-auto px-4 sm:px-6 ${isMobile ? 'py-4' : 'py-8'} space-y-4 sm:space-y-6`}>
|
||||
{/* 데스크탑용 타이틀 */}
|
||||
{!isMobile && (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">프로필 관리</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">계정 정보 및 마인크래프트 연동</p>
|
||||
</div>
|
||||
)}
|
||||
{/* 프로필 정보 카드 */}
|
||||
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
|
|
@ -310,14 +320,14 @@ export default function ProfilePage() {
|
|||
</section>
|
||||
|
||||
{/* 계정 관리 섹션 */}
|
||||
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||
<section className="bg-zinc-900 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<User size={20} className="text-zinc-400" />
|
||||
계정 관리
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-zinc-800/50 border border-zinc-700 rounded-xl">
|
||||
<div className="p-4 bg-zinc-800/50 rounded-xl">
|
||||
<p className="text-sm text-zinc-400 mb-3">
|
||||
회원 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다.
|
||||
</p>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue