diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index e79d9ed..5bb5ba6 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -363,4 +363,35 @@ router.post("/logout", (req, res) => {
res.json({ success: true });
});
+/**
+ * DELETE /auth/delete - 회원 탈퇴
+ */
+router.delete("/delete", async (req, res) => {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ return res.status(401).json({ error: "로그인이 필요합니다." });
+ }
+
+ const token = authHeader.split(" ")[1];
+
+ try {
+ const decoded = jwt.verify(token, JWT_SECRET);
+
+ // 연동 정보 삭제
+ await pool.query("DELETE FROM minecraft_links WHERE user_id = ?", [
+ decoded.id,
+ ]);
+
+ // 사용자 삭제
+ await pool.query("DELETE FROM users WHERE id = ?", [decoded.id]);
+
+ console.log(`[Auth] 회원 탈퇴: id=${decoded.id}, email=${decoded.email}`);
+
+ res.json({ success: true, message: "회원 탈퇴가 완료되었습니다." });
+ } catch (error) {
+ console.error("[Auth] 회원 탈퇴 오류:", error);
+ res.status(500).json({ error: "서버 오류가 발생했습니다." });
+ }
+});
+
export default router;
diff --git a/backend/routes/link.js b/backend/routes/link.js
index b46bb0b..48b55ec 100644
--- a/backend/routes/link.js
+++ b/backend/routes/link.js
@@ -209,10 +209,41 @@ router.get("/status", async (req, res) => {
return res.json({ linked: false });
}
+ const uuid = links[0].minecraft_uuid;
+ let currentName = links[0].minecraft_name;
+
+ // 모드 API에서 최신 닉네임 조회 및 동기화
+ try {
+ const MOD_API_URL =
+ process.env.MOD_API_URL || "http://minecraft-server:8080";
+ const modRes = await fetch(`${MOD_API_URL}/player/${uuid}`);
+ if (modRes.ok) {
+ const playerData = await modRes.json();
+ if (playerData.name && playerData.name !== currentName) {
+ // 닉네임이 변경되었으면 minecraft_links 및 users 테이블 모두 업데이트
+ await pool.query(
+ "UPDATE minecraft_links SET minecraft_name = ? WHERE user_id = ?",
+ [playerData.name, user.id]
+ );
+ await pool.query("UPDATE users SET name = ? WHERE id = ?", [
+ playerData.name,
+ user.id,
+ ]);
+ currentName = playerData.name;
+ console.log(
+ `[Link] 닉네임 동기화: ${links[0].minecraft_name} → ${currentName}`
+ );
+ }
+ }
+ } catch (modErr) {
+ // 모드 API 호출 실패해도 기존 데이터로 응답
+ console.log("[Link] 닉네임 동기화 실패 (모드 API 오류):", modErr.message);
+ }
+
res.json({
linked: true,
- minecraftName: links[0].minecraft_name,
- minecraftUuid: links[0].minecraft_uuid,
+ minecraftName: currentName,
+ minecraftUuid: uuid,
linkedAt: links[0].linked_at,
});
} catch (error) {
@@ -384,4 +415,70 @@ router.post("/cancel", express.text({ type: "*/*" }), async (req, res) => {
}
});
+/**
+ * GET /link/skin/:type/:uuid/:size - 스킨 URL 조회 (캐싱)
+ * type: avatar 또는 body
+ * RustFS에 있으면 S3 URL 반환, 없으면 mc-heads에서 다운로드하여 저장 후 반환
+ */
+router.get("/skin/:type/:uuid/:size", async (req, res) => {
+ const { type, uuid, size } = req.params;
+
+ if (!uuid || uuid.length < 32) {
+ return res.status(400).json({ error: "유효하지 않은 UUID입니다." });
+ }
+
+ const validTypes = ["avatar", "body"];
+ const skinType = validTypes.includes(type) ? type : "avatar";
+ const skinSize = parseInt(size) || 128;
+
+ // S3 URL (타입별로 폴더 분리)
+ const s3Key = `skins/${skinType}/${uuid}_${skinSize}.png`;
+ const s3Url = `https://s3.caadiq.co.kr/minecraft/${s3Key}`;
+
+ try {
+ // S3에 파일 있는지 HEAD 요청으로 확인
+ const headRes = await fetch(s3Url, { method: "HEAD" });
+
+ if (headRes.ok) {
+ // 이미 캐시됨
+ return res.json({ url: s3Url, cached: true });
+ }
+
+ // 없으면 mc-heads에서 다운로드
+ console.log(`[Link] 스킨 캐싱: ${skinType}/${uuid}/${skinSize}`);
+ const skinUrl = `https://mc-heads.net/${skinType}/${uuid}/${skinSize}`;
+ const skinRes = await fetch(skinUrl);
+
+ if (!skinRes.ok) {
+ return res
+ .status(404)
+ .json({ error: "스킨을 찾을 수 없습니다.", url: skinUrl });
+ }
+
+ const imageBuffer = await skinRes.arrayBuffer();
+
+ // S3에 업로드
+ await s3Client.send(
+ new PutObjectCommand({
+ Bucket: "minecraft",
+ Key: s3Key,
+ Body: Buffer.from(imageBuffer),
+ ContentType: "image/png",
+ })
+ );
+
+ console.log(`[Link] 스킨 캐시 완료: ${skinType}/${uuid}/${skinSize}`);
+
+ res.json({ url: s3Url, cached: false });
+ } catch (error) {
+ console.error("[Link] 스킨 캐싱 오류:", error);
+ // 폴백: mc-heads URL 직접 반환
+ res.json({
+ url: `https://mc-heads.net/${skinType}/${uuid}/${skinSize}`,
+ cached: false,
+ fallback: true,
+ });
+ }
+});
+
export default router;
diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx
index a81958e..86bbb58 100644
--- a/frontend/src/components/Sidebar.jsx
+++ b/frontend/src/components/Sidebar.jsx
@@ -15,7 +15,7 @@ const Sidebar = ({ isMobile = false }) => {
const [toast, setToast] = useState(null);
const location = useLocation();
const navigate = useNavigate();
- const { isLoggedIn, isAdmin, user, logout } = useAuth();
+ const { isLoggedIn, isAdmin, user, logout, checkAuth } = useAuth();
const profileMenuRef = useRef(null);
const menuItems = [
@@ -69,16 +69,44 @@ const Sidebar = ({ isMobile = false }) => {
fetchLinkStatus();
}, [isLoggedIn, user]);
- // 서버 상태 확인 (socket.io)
+ // 서버 상태 확인 (socket.io) + 닉네임 변경 시에만 동기화
useEffect(() => {
const socket = io(window.location.origin, { path: '/socket.io' });
+ // 닉네임 동기화 함수 (변경 시에만 호출됨)
+ const syncNickname = async () => {
+ if (!isLoggedIn) return;
+ try {
+ const token = localStorage.getItem('token');
+ const res = await fetch('/link/status', {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ const data = await res.json();
+ if (data.linked) {
+ setMinecraftLink(data);
+ // fetch 완료 후 checkAuth로 user.name 갱신
+ await checkAuth();
+ }
+ } catch (error) {
+ // 무시
+ }
+ };
+
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]);
// 토스트 자동 숨기기
useEffect(() => {
@@ -150,7 +178,7 @@ const Sidebar = ({ isMobile = false }) => {
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-300 hover:bg-zinc-700 hover:text-white transition-colors"
>
- 프로필 수정
+ 프로필 관리
{minecraftLink && (
diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx
index 618a1f7..d8bf58a 100644
--- a/frontend/src/pages/Admin.jsx
+++ b/frontend/src/pages/Admin.jsx
@@ -2,31 +2,46 @@
* 관리자 페이지
*/
-import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useEffect, useState } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { Shield, LogOut, Settings, Server, Users, Loader2 } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
export default function Admin() {
const { isLoggedIn, isAdmin, user, loading, logout } = useAuth();
const navigate = useNavigate();
+ const location = useLocation();
+ const [toast, setToast] = useState(null);
// 권한 확인
useEffect(() => {
if (!loading) {
if (!isLoggedIn) {
- navigate('/login');
+ // 로그인 후 다시 admin으로 돌아올 수 있도록 현재 경로 전달
+ navigate('/login', { state: { from: location.pathname } });
} else if (!isAdmin) {
- navigate('/');
+ setToast('관리자 권한이 필요합니다.');
+ setTimeout(() => {
+ navigate('/');
+ }, 1500);
}
}
- }, [isLoggedIn, isAdmin, loading, navigate]);
+ }, [isLoggedIn, isAdmin, loading, navigate, location.pathname]);
const handleLogout = () => {
logout();
navigate('/');
};
+ // 토스트 자동 숨기기
+ useEffect(() => {
+ if (toast) {
+ const timer = setTimeout(() => setToast(null), 3000);
+ return () => clearTimeout(timer);
+ }
+ }, [toast]);
+
if (loading) {
return (
@@ -36,7 +51,26 @@ export default function Admin() {
}
if (!isLoggedIn || !isAdmin) {
- return null;
+ return (
+ <>
+ {/* 토스트 알림 */}
+
+ {toast && (
+
+ {toast}
+
+ )}
+
+
+
+
+ >
+ );
}
return (
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx
index 1199988..34a96ae 100644
--- a/frontend/src/pages/LoginPage.jsx
+++ b/frontend/src/pages/LoginPage.jsx
@@ -3,17 +3,21 @@
*/
import { useState } from 'react';
-import { useNavigate, Link } from 'react-router-dom';
+import { useNavigate, Link, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { Lock, Mail, AlertCircle, Loader2, Shield, UserPlus } from 'lucide-react';
export default function LoginPage() {
const navigate = useNavigate();
+ const location = useLocation();
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
+
+ // 로그인 전 접근하려던 경로 (Admin 페이지 등에서 리다이렉트된 경우)
+ const from = location.state?.from || '/';
const handleSubmit = async (e) => {
e.preventDefault();
@@ -25,7 +29,7 @@ export default function LoginPage() {
setLoading(false);
if (result.success) {
- navigate('/admin');
+ navigate(from, { replace: true });
} else {
setError(result.error);
}
diff --git a/frontend/src/pages/PlayerStatsPage.jsx b/frontend/src/pages/PlayerStatsPage.jsx
index 97bbdb4..1be1fdf 100644
--- a/frontend/src/pages/PlayerStatsPage.jsx
+++ b/frontend/src/pages/PlayerStatsPage.jsx
@@ -9,15 +9,26 @@ import { formatDate, formatPlayTimeMs } from '../utils/formatters';
// 스티브 3D 스킨 기본 이미지 (로딩 전/실패 시 사용)
const STEVE_BODY_BASE64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAADACAYAAACTd+TuAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO2deawd133fP2eZmXvv2/get0eKmzZqsSTLsmLTduzIrmM0rhO4qZMUcmqkjZu2SfqHawRFEKSCEwT5yyiKpkmQKqnhRkZqp4mARoZj2DGR2opsSd4iidook5RIkY/rW+42M+ec/nFm7rvvvXtn5r6FpNx8Af7Bd89s3znnt5/fCOcc/4D1Q17rG3ij4x8I3CD+gcANQl/rGxiEuw5OSuBjwHuzP30N+OwzJxfstburwRDXkxK5+9DkIef4ReDngUOrfj4B/Cnwx8+cXDhxde9sOK4LAu+/deYQ8BDwC85BaizGFk62zwCfuh6IvGYE3n/rjAb24on7V6t/r0jknwCfAs48c3Ih3ZIbLcFVJfD+W2ckXu7+LPCbwOEqxxnrSI2l4F5fBH5bSfH5QKv0qZcuXTVZeVUIzJTCXiHEQ1qKjyu1VvlLIdBS4BwkQ2adc47UuIGzUgiBVhIlxcNks/JqELmlBGbEzeIf6OO9iwrQUqKURApBqCRait5xzkFsbCUi+4hbPexhIXhoLNBnjz47t2VEbgmBdx2cDIGfxpshvzRojJICJSW1QKGlQIqVBHQSQ2otqXUoKRCrfjfG4vrOtfr3QEmUn9V/hDeD/uLos3PxpjxgHzaNwPtvnQmT1MxY6z7h4IPAXWsuJkBJOfCBtRQIvLzrpgZjV96XkpL8EGMtq29bSYlWgkgrgr6xfXgG+CLwn4FLm0Xmhgm8/9aZBvAh4DeAewBsJvTtkHMrKXrLdxBWKw1PTj+BK3/3S3j590BKQjWQxBzfB34nsfavvnHsQmvkh+7Dugm86+DkLgEPKSV/WQ9QCkL4B0mMpROnDLpKvywcFc45jHW9Wb0aSgiCVbK1H8Y5YmMx1v0+8KmnXro0N/JNsA4C7zo4uQOvFH65/+9aLc+SUEmCvodyztGKDZ1k40R6BWJXLHGvSLxMHaSU+tFH3Oqfft8699C3X758ofQm+u+9CoGZUvgQXin86qAxSgpkn1JQq9ZPYv1NdxKDtW7N8lZSIqUYpE0BrxS8jLR0krUyMr+HUClqgVpznthYTDZrByEXOda638Mrnb965uRCqZwsJDAj7neA9wH3DRqjlX/w1fKsX7OmAwgz1vXJuLVKBZbFwCCl0EkMibF0UzPw3j2J/t7SPo3dj4wwUjPQyvk28DfAbxQRWUbgOLA4dEAGKb0tJoXXpH4myBUEJtkMAD+LUrOSwCKlorPz5SSnxhL3aeKBSiWbsYOQGjuMtEGYeObkwtKwHzclnGWtI7YGIaAeaCKlVswYPxslsfHLb/XNG+sw2fGDZKF/AelQpeFtSlV6n4PkZw7ZZ0aNgk2NBzoHrTilnaTUA0090Di80E7tytlhBswC5yAxltTaHpGDHjoVrqc0RoEQgkArdN85pRTobKkbYzEDF/twbJhArWRPluToERmnfmmukpGC3KgWA994TmQyZJk550hSRypcNvtWylDBstIBSFbJYCkEY1GAda73Ytf9/Bs5WAhPINDTrP2zyuHlDYYVJPYrkI0g94lT41+GVtJ7IqvkX6CyAEV2SZ39nlhbSOAg5bgam7aEpRRI/EMMEtLWOgbrwhWRFJzLlczy8VJ4JZP/nlqLGSJHU2O9HA7UShJHWO79SrEMIxMopVixXAeeNDOqy7TdoEhKPqu1kl5pZNfs/z33MFLjsHblazHWsdRN6CSGeqiIdLlyAXrXCaWqRFzvWSuPzC4SZjc0TJv1Q0nBajPN36h3/AfafngCwz7bL7VeETnnemR5hSAA2TNj/BoV1ANFPVADz7/yWqInL0Pl3c5udfMG2MAS9jMH4tT4YGhFf9Y6hxYMNZy1lARSrDCDlBQETmAcA2WWloJ6EPSCsmVweCUlhWAs3JgUKzxaQKlSz2VbaqyXU0KsWHL9cM4RJylxkqK1QktJ1PcAy9p3ObwFkDq3JnwFXjYGmbyqsuhMpnVzzySQw33mqigkMAp1z0RZLbQH3qCxGOjZVkUrKE0NRlgQy+GqHNY52rFZYTgPmuEyk4dVkb+gKihb/jlK568UAqlET6irTIkUzcx+z8Rmxuow9GffpJSwyuFf/t0NC91vCiKtcRjirfREtBTUMyUSG0tc8jatdSSpzzZqpRAlRA6a5QIyE6Z4Rq8HSgqiQHPnnllu37OLY2fP842XT4x0jnVL0DzmZ60jtbbUtEmNgcGBk6GQQhAG1cwQ47whX8Xe01Kyf8cM9x24gZ1j9dFuavW5in50jtK3nttOVYnsx0YnlGNlgFRLQVDAn5KSG3dOc+TWmziwY4Y4jmm3h0f0tZQEqvgFFhLYTVIvA7NwU9EDSykIhKJrqxcICCEItcI6tyLLVhXGOtq2fFrXw5C33nyI3VOTzG7bVjq+FgS8Zf9edk2OMTs5UTi2dAlb57AmM1OkRImUSOtK8khk5BfNytwFVFJincVkccLVR+S+c2XtqBQ3zExz697d3HHDXqJAY4wlHhKAFUJww7ZJ3n3rIW7bvbNypGckGZhay8XFLqFWTNTDnldShEBrbwZZU0ikEKCEN85zf9g6l9mOhm6SoqRgrB4VP5BS3LBjO++48w4O7pzBdDuF44UQKKWJooharcZ0YzSZuC4lEqeGi4ttQq2IwgAhROGMlFIgpcY577uWRWNyfzg1loVmPwHDL9KIQvbMTPPAPXdx4+xuAJxJCp9Da0WjMYbWnoY4HlHLscFoTGIMIvVTPc+NFEFKST0KerNqhLD6QGglqQchb735IG+75RBj9QYqqj6DpFQUrVQvLq5SOCs1FmFZVgoDqgf6bywKNXUhiJTEOJ8zqapEaoHmTXtnObhzO/fceKB0vDfGzdBE/6D709ova6WKKRo5GiMEQ4mBlXFBky3XItmnpEAhfBLeFhvnM2MNjtxykEM7pqkHAVEUFt6vMY4kNSNoeEEQBJWIyzEygWO1iNRaunFKWajB18HQsxGLhuch/kHGtg+oKraN1bljz67S+3QmxaYxJkkqiYna5DZuf+9PcunUcU595/HS8f0YeQmrLCkUBpokSaliN+fGdh5pHpYHXg0f89OlsrUHazHdFs6UK4N2EjB7y2Fuffs7OXDfu1BByMtf//LmEli0XAXeROkmo1XWNttdb0AHinoYZCHQ4devTB7gnC10F1MriVPFC+d2cnxuB+/d/xY+8Pb3Dh3vXHnupjicFeielzBqvrQIzjm6cZpp4pBGqCsHZD0EWiuisNgm7L+eMSnH56b5zulbKo1P05Rut4Mpmc3VwllaoZzDmM0vxkyMZb4dEyhfV1OMXDvW0FqXhracgzRN6Ha7pGmKMcVumcORJEkl4nJUloGeyNHcf5+/tVnRUPEMy/PAeSqxf3wjDLhn/x72b59hbGy8wnW92dKNY5rNZun49vwlnv+b/8PZ48dotVaO/8QHPvYH//rRR78G/IX78IevrD52y3cq5b60MZbxWsj2mToXlpo048FeQp4iMNayfazBfYdv4NCOaRph2PMYBqEVa8ChZYzNzBZbsEVCK8v8yb/nyT87wavf/TtMEhPH3TXjUql+Hvh5nPuE+PznnyJJftt99KOv9M5TmYkNwucgFD/55tvoJCnPnpnjudfPkzK4Oso5HxW5c+/uoedMrSTuBrzw+gwvnJ1m58QiP3rLicL70MoyO7nI7bNz7IianHiywp2nKSTJXVh7F/AL4pFHPgt8yn30o68UEtjuxGitCiun1oNaoHnrwb38yI37uNDq8NSJ1zh9ZaGya6eCkB033sZS7U7++I9fqHRMkhq21Rd54PBxdoyXL2tjLXGa4rpdBuzJ/BjwMfHII58tJDBOTS/8EwaaKBjBJquI/TPb2D+zjVOXrvDN46c4fWVh6FgVhOw+fDe3ve8n2X3rXTz1+HFgOIHWep/7/HyTC4stdk13uX1HMHx85oIudTq04hgHuDACISFJ/LJYadZ8rPISjpOUJEmpRWGvoqAMzkGSpIgKSuTAzDaUEHzuie/4EFPfeCklYRiy9443855/8+ul1+3GCa/NXWKh1WF+qbyG3FjDYmuRZqfLQrO9doDW/l+SQLyy1nJkGWizt+CzaAKbZ9OKxpvclrQopQpNBO83L5dsNBoNtA56Dv4waGlJkiWOnTjDxfkln4MpQJKmtDptrEtpx+0sQjR6dGhDSiR31qWwvcxZEZxz1KIIYy1JviSGjvX/gqA4YKClZdfkErfPziHM6zz7ynzh+CRNefXsHN978WVqkeae2w4Wji9DIYHvOnyQ1y8vMre4xFJneL21dQ6bGsJIcWB6iovNNs14+HglJSqKCs2MMrSvXKKenuE9h19h14SvwD2/xkpbhkRw9sIljh0/zrmLlwA4uHfn8PFCoFRxoBhKCLxp5ww37ZzBOsfxuUs8d/ocToiheQUpBD92y0ESYzl+4RLHzl6gGyfDx6+asVopAqVIhiw/E8csXTjLD775NV755lHi5iK7CpwLHw4VBEISCMnFhaUeeQPHZ753FEgCvYk5ESkEt+7ezuHd2+nGKc+cmePU5Xm6yeAHDZTk9t07uHN2Jxc7MU++8ionLgy/8Ry7J8f5Zz9yN69fXuDbJ0/3/m6tJY5jTj//fS787n8oPY/A51dCIVEVkqd59QPAeH00qTayDNw53uC9hw9xfqnF358+x8tzF4fOMIB901Pse+sUr12a56VzF3A4RMFDHdw+zcHt09xzYA8XFlu0Wi3SNME514sSD4MA9EjEeVczTnywJAwkw42cwVi3Etk53uB9t93Im/bs5PxSi2+fOlMYqd43M8Xe6Qk6aRcpJKEMCmOCjTDkwPaQJCnfE7jUbvPcKydIYkNd1ErHCyFQWmERtLujJ5L6UUagBV4GhsaAdk+Os3tynNt37+C1KwtopbJ8yGA2jTMYZ0htikAi8XJv1Ip7Yw0X5xc49oOTnJ67QDdJmKqPc3DHnoHju0mMcYYg0MjMhi0L6p4f34YbNkYIUKqYwP/054+3fusj77wD30XjNyggUivJjTumiaLIF5enKcaYoUvW4UhtSrPdQQpBIwwZi6JCIn1ReUqz06TVaXHu0hVeOf360PHGWpI04dyVS1xcvAICdFC+6M6Pb+Mrt72Np/fdgVl9PxlxBAFIWX2z4W995J2hFOLBiSD4j/hd6GtqJIQQNOrLaUXrLF3TJZABSiq/6TBdtvSNdTTbyxFtrQTTYxFKBEixMjZojCE2CUYsL7nzlxf4zvMnev/PZ6C1lsVOi3PzF5lfWt5kpJRcQeCumW3ctM/P2LYKiMfG+Pqb38XT+2/HrN64Y+1ysVAfqeva7vpfHnzgPuATwBH6ZuUgAnPClJBoqYlNQl64MYjAqYZ/QCkUSgQ4641fYwwIcHr5flcTOF5rsH18ivOLl2l1O1hjSfpSDoMInLrpZl5rbOOJ7Qc4s/8m2Ldv+UFzaz5JfEQmCCBcadivS4mMNRpngO85mDTG3GKMKY3gGmdxNqWmI6yzJDYp3BVknT+nSappU5Na5tNFljrlvm9Sb3D+jjdz7N638Vos1tZdW4vsdHBpittIdVY/xCOPyH1Ll+/76ItP/totfufmLb6iXqGVwlqLkBIpZaGHoYRCCYWWGiVSusoWJuGHwRdkOp8ZzFKmUkmKHre1c5ZT/+hDLNxwgPl9N/o/Pvss2Cy4ay2y1UI0m4gkwUZRIYFCiHICxSOPSOBe4KHXxqd/6tP3vp+3zZ3gJ04+y2zLh56U0oyNRWjtyzbyvIIt2iOCIJCKRk1hjKSbWESF9HdeeNRNLO1uWniNHJ64f8K5+9+FKUhEyXYbeaXAH8zHSUkQRQgphxMoHn10Fuf2otRvYu2H8ymSSsnjszfx5K6DvH3uJB+a+wG7nUFnhTxCCMIwJAgCunGXpJX6yixXMCuVoKGU38aaWr+7c1XcUQhfTNnqppWSW2lUozU2xYl3/zhn738XJlplH1q7rBgqQGqNDENUvY7qiwqtIFA8+qgE7gTeD/w7hDhMreYvlCTLFwUSqfj67E18ffYmbmrN88G5H3BL6wp7O83sgQW1qEYtqhGnMUvtpVI5KYRgsZ0iBNQCv+9NCBDKISU+txIXP3Br23YWdu3l1H3v4sqNhzHT08s/rlYKJRBSIrUmGB9H1QYb6Dojbhfwz/Fb+j+8ZpSUkLtQaQrG+H/Z23ulMcXvHbqXyBruXTjP266c5b75OcayWamlphE0MDIlrXDjzkE79puyJxvlYjoMNPt374LDd/A/732ANBzijRgD3bWJozVQCjk+jpqeRhS4jgBaPPro/8bPutvLz8xydNZanylIEmxGSlcqvrltlm9um+Xm1jwHm1f4wKvH2NlcWKNYEiF5XdewQcCO9vAwfhHqUcSPvuUedk5PsXv7DN8a2z6cvCpQCjk1VYk4gIlOC43vMDQSpFIEuSxwjrTTIW02e0QCHG9McbwxxeMze3nbuZN84NVj7G4tkAjJi9EEX5rYw/PRJPV9e7l/4QzvOf4dphcuV7p+qANmJieYHBsf6Lk4KXFRhKtAAgC1GuzejZycRFeood575Tz/+Jm/Y3b+IoK//MuRDIggitDB2kCAtZa03SZtNgclX6hZwx3nX6V9/iLPR5PLP9x0E0xO0og7vOXkc9zz/HeZac4TpgmBEr0l3FWaeVljL4KpAcS9riK+PbmLz+67A6d1Vuaql0UPePGTL2HnkEmCFQKmpkAIdBAQDpF1450Wb3n1RQ6fPcndp48j8v4Mo5AHkHS7mCRBhyEys5FMmpImCc5aqNdXKh0ArelozXeCm2FhcEK9Fdb4xs338o19d3Bg/jzvfOFp9i1eJNaOM41tfG32Nsal5tdff6Z3TFso5lTI39S383htG0uNcVxQEpDKiBNxjDAGu3t34V6O8U6Le199kXe/9F1m5y+u+X1dnoi1lrjZ9CQJ0XOse+hXOqNASqjXOVU/wKnZA4w3F1EXL9DU/lx3t7yN1lYB352e5YvbbuCVhQFZtCEQaYpsNhEV6l5mL81x84XTvPvl77NnfngvntEJNMYT138TaeqXy2oiN4hmEKH08otoa80TO/bzV/sO8+LEdsTSEnrhdMEZVsHaQvLGOi12Nq/wnu8/zptOHKOWpVOHQYgCQ3ognIPOkG0DuXnTaIx0ykqXzZTCC1NTPD9WoemllCO9zD3zF/jxY0+yZ/7iytk25HghJGEYEIbR9dkGOYeTEtNoLCuFIsc+Vxq1mn/wsnSar33jo9/6a95y+ji6wo6n3MsKwxCZhbvKCayyYW6rIES5UsiIc0HgySuJnuTEiTgGa7nt7MlS8pRSRFEta+Gy8vzFBOZLNjeetxK5xh5FhiqFq9fLScsgjEG0WsvXKkAQBNSiiHq9jtbDX2I5K9b6epBcUWw2+s8vhDeDqqJsWQ+6VgF5UkqUUoyPj1OvVbuP6ozkD7rZiGMv8GG4qJDSv7xRyBoBvlhK0RgbY6wxNtKxawlMEn+zmy33nEMkyWhbWqWsrhTWCa01NeV7H0oxugm2lsD+5boZb9w5RLeLXFqCVmu0TeujLtF1IAwCZLr+4vnBSzhfrhsxiq0FY1Dz817jUbyvSWSyafP3AawPzvk0Q1HpHoAWQgzfTDJI4JZtlrPWz+A09VqvSG5K31RRtNvIJMEFAWZsNBm02XDOEccxcRwTBJpaiTLRtUaDNEl8MKAsvC2El0kZQSuIVMov+3YF31RKmJiAXbuQ3S5imHdzlZGmCUtLKbZnF5brWC2yBIkOAk9kp1O810xKnxvV2pNordeiSpXnF6T0oaNduyCfaVUixFcJ66lX7FHcI3JmBpumJEtL2KRgx3dOZAWIMERu2wZhWPZ5C488HCZE5WtcK6yZo0IplFKoKMJ0u9g4xsRxMZkDILRGKoWemkJEEUJr/4aH7RzKZWens5wt22INvBnQ061FLjcGl3mqKEJFEQFgul2SZhNXpHTAp/6iCF2vI0o0WJQmhJ0WnU68XDoxUbyf7XqD/tWv/i+euOlunrzxTq4MIRKWybTWkiYJZpXSUVoT9EWpixCmCXecPcH7nn+ar07dwLHJ8k3UI/vK6/Gt1wG9Y2meD33/6xx55RmeuPkunjxUTKSUkjCKsJnScdaORNxtr73E+154moMXz/o/Tt0w/ABfv7Eca8w9kyLk43NnoCgyXqGpRBl6MnDH0hU+9L2vc+T4M5yf2MZf3/UOfrBj79ADcyKrYOeVC7z9+afZP3ea/edHiyAPDeAOgjHVxmdpCBcEG3YR1yiRHUtX2LF0hdvOnuTlXft5fs8hnjp0B/P18m2m/QhMyu1nfsDbnnmC3ZfPM94u3582EpyrFJZagcxycJuYehhqKUrnOHzuFIfPneIdx/+ev7v57kpEBibl9tdP8P5j3+LA+TO0qxjWoyBf1kni/1VBZhY5a6vniitCR1ENY4pLLnYuXuanvvu3HHn5+/ztoTs5uXs/p3btWzFm18Jldi9e5P3PfYtDF3zZbdH8kNmnKyrXRufjcjOnCvIlnTsGG9jYMwy6lgnlvEeAtXaoRb5z8TI/8a2vYKTipX038fW734GUkrtOH+etJ59nssIyVVKitPYNGUVJ9468nDaPDI0qE/PZuoXoLWGtNVqP9za1JEk8lEhlDbefeok7Xn15RUnvMOSVWvV6nW5JM7DsgBWF3FXG2yDAXQOvZY0MlFJSq9UIw7CUyDIIoF6rMz4+ThAEGGOKCcwza6MQF4a4MCwtxd0qDFcifURaa+l2O36HZQVIqYiiEKU0qsqDSelJqNUqVTQIY5DdLk5K7Cg5lIrIO4OUxQIBdKfTyZbvYC5lVves9ThJkpCkKdaYNUEBKX1rgDCMUKq8i3gOF0W4RqNS9FkYs1ya4VxxkksIsjbCle4DfCYuDKPCfcmrobvdDt3uMuuFm5q1JgwCyJZjakxGfkAYhpVJW4FR8h3OIcoKNHMxkNUwjhIuE0KORB70LeE0TUnTJaamyuvjwCebtQ4q9nFxvQY4WwYpl4O6+dLbRLMlTdPeauzHlmbL+4mr2gloJOTLfosS/7lJ1+12SNOUKKpRW+WLbzmBrVaFDwda2yu1GAlKlQcX8nF57nkEzyhJEjqd4vGj9Q8Ugiiqbci0WXPObhehWuVKYT2+b05wrpy2ILc88gzstxHTdLQo9UCU7dXo931HrYy4CnnldS3h3Ea0dussf5EVcbqqM+8qBVBXY0MysIqhORKs9VUMrRai08EpVZwn7p+dxpQHULcA11WBpUhT1MW1hdxrkO82MqaaEnG9BmKV7qM/HlCG64rAUjgHceyXt7Xl5OXE5fnrElHgnDdZ4ri6knxDEJj7viKOERWMcdFuoy5exGmNqZDlM8aQpClJkhCUVcR6vAz8d+DodU2gMAa1tOQLj0pie0HTUrt0hXa940l2zvvYw5CJgU6njc5eSnmLP/cl/LeVX/uZT3/uNajyNYeS5mJbCudK93QELcvO51rMfq9Jezzl5XeVlJdkxOVioOJS/RLw291u94l/8d/+csUBJR9kcTSbSwRBXpl+jYgcgMmLmrHLlh3Hz1O74gMM7QK3XHS7yE6nco008ArwHPC7wBMff/iLAw/SZVv0l2OBMWEYEq3euHwVEcSSyddDdp+I2DYXIJKUxAz//LEwBtHpeDGQLesK+DLwf4HPfPzhL75WNliPjY3jnKPT6RR6Fp7I7jUhcHJOsO8ZReOKoL6wfP1hr33qfMC+F+osTaecfFPlT6Z/2fmveT/+8Ye/WLm7uM6X5djY2NZETDKIrB9+lQ3XALrr2HHcsed4RH1BoEpMOCEUMxci7v2qprGokEaQhuVLNRHqy2TE/cpnvzJaW3ZWycBK4fcRIcTyFxKcg6Wl4i+O665j+/GU3c8mNBYkgSqe8UIoAtVA6zFwgvBKuZlTayv2nBpn+9kaX+Fn/+XjX/yFMyM9VP/9rvfAKpBSMj4+3tvdUzTDxy9C47Jl98ttavPZzCmpmlcyJAjGkMI/hnXDzx+1FRPzIftemaDe1NTamzNZttwOXL01ajXGLsLN3xNMnSVbpqOErGSPvGGYPl9j6nLInpPjm0ZaP665Ib33OYEpbnsKCKRUCCExplpISwjFzgsTzM4ppN26vX7XnMBi+I9ZyazXoCtYor0jhEIpjRAKRlYJo+O6JFDJiECPI2VQukQBnDMkpoVzhiiolhTbLGwZgSYLhlZNE0qhkTIk1BMIqQvbhAI4LM6ltONFrE1wzqDV5ifZy6CtNaWCviryD5kkSUKaJgghmJiYLBwPikCNI0WAEAopi6Mh1nmyjOv4L9hcY+ilJe/rRlG4YSJ9Fm65QmtYot13ojSkSYIkQopqqQHrUhKzxPWzIQy039rU7fm6ZVubNooka5FS9RtvK+G4nsiDPhnonKPb7W4JgcakdLtdkiQh3oo9x9cQJV/1EtRqdYxJK1dm9cNay+LiYrbz0ZXG3sQ69utea5SqSN/4Ospq+7ojB1htpW4Y0pssm6TMriYk8Hl84LAQSinq9TpLS4t0Ou0NVyYkNuT00iEStqF17Q1JHoD8+MNf/Dngx4BfEUL8edkBuaz0RI6+TfVSZyevXLmdo69+kKfPvYeFePgXFd4IWNMG+QuffPB2fAfLX2FVT0HnHAsLy46rEJLJyWU7z8u8tb0AE6N56dJuXji/n8TuJLVB73itqwdoje3Q7q4NkAokSkZo1UDrkZXgDX/9jQc3L5z1M5/+3PPA81/45IOfxzfc/k3gI+s5+VxzkmfP7edCa4KluIYQilCN2u5/OAQSKSOUjBDIa6KEhiqRn/n05+aAuS988sGfA+4CHqJis8a55iTPzu3n9MIMidl82SZFgJL+hYi1X9y6qqjcyfwLn3xQpml6T7O59GvA/cDh/iWcGsXxCzMcez0cSpyfgVN9/x9tCVtrSNNWNtPWejm+Tnvk2pgNLeF1tYJ/+OMf3AU8KKV8d4t9P31haYLvvHaQhSRncKYAAAZUSURBVE5InPb3YRYIBC4Lkm6UQOcMaTo8ZP+GITDHP/2x/7q3HQenY+MlgcOuINDnKyawNsbYDghx3RBorcE5g3Pmhq9+8xevTU5kvl1hlxISJWtIGWLdJhRkbgD+e8EGa9NNi+RctYCqQKLE1a3dy5GaNnG6iESh5Ob6+tdlRHozYF1KmjZJTTvL1jnEFrzAHzoCnTMk6RKJaV6VgOsPFYGp6ZAm1YjzWT7/byP4oSIQXDF5Is/y6U3zWn7ICBwMISRK1NC6gVKbu7PgDUOgc4bUjLbXLidOyigz6Tff7bvuCfRKoUlilhB4w7wMUoRoWQOhStOjG8V1S6AxHYyNScxST64VCXwpFcLVEEJf1QDDdUWgc5bUtknTFsbGlGXgfNMK1VMK1hpcxe+0bxauEwId1qakpkOcllYaIfq0aZXWTX781uRcrjGBDmsTjEnxJkh5B02lgnUSt0VdgLfkrBXhnMOY8gCDD6DWUTIoLf0ALw9l7+ux/58qESmCrOAo6lMKxcrBexf19fVuWCeuKwLz5JAUUaWytoHnuMofTrhOCBQopQGJMG+s6oRrTKAnLlcK10O52qi4pgTmWrIMK5XC9YXrZAkPwsrZeb3iOiTwjUFcjuuKQCEkQVDjjUBcjk1WeWLDEd43EnmwyTNQIAjVJMbGGHftPjDgizm9h7PZAdTV2IIlLFAyQnH188A5cb6o05UGD6xNMHZjDdG2UAaKytX3G4W1JtsrUq0IPTFNkmTRVyZssGh9QwTG6WWkCHuVUlcTq2dbGaxLSZJFUtveVIN9QwQ6HMZ1MSZGyRAltp7IZeKqbYSzLiVJF0nT1oZn2yBs0hJ2GNvF4IkUQm96GUdeDFSVOOO6pGmKtfGWEJdjk2Wgy4RyFyPaPoa3ASJHLQZyzmBdgnHdSjs7lW6ioyul44qwZUrEOYuxnfXU6/WfpdL+YOviPtlWPtvCxllq4ycQMkGIjcnDDRE4NfsNks52ukv7MenV/RqXcxbj2pWSTwA67FCfeA0ZnEOIzdtIvCEChUgJ6+cIaudJOjuvCpEOi7GjETex7Sz1iUsYE9Ptbu4u7E1ZwkLYFUSm8Tbi1uxmnLoH62KsSzMZW2GZ1s9SazQZ3za/4WVahM115TIiw/o5auMn6S4dwnXH123a5HK0qlKQuk0QXSQaO4NUHbRWhTWB1toN98rZMiUiVYdo7CQLrdmRbUTnDHG6SJI2qTLbpG5TG3uVoH6u0myz1mabwje+nDdEYLvdJgzDkoY9K23EIiJHJk61qY2PRly3290U4nJsiMBWq0W73UYIQb1eX9OkeiWWiRRCoGQdLXy9sq9hXsiM5CryzYsIqbpQgbg0TbOWnpsz6/qx4SXsjV1Hs9mk3W7TaDSyNvHDTu2yLf9NUtOCxP+tDGF9Dh1eIajPZbOt/Bjf3n5re59sqgzMm07kb1xrgdItTDqsk2QxCVJ1CepzhPWzKN0uHX8tsIWeiMOYFhM7niLu7KKzdAA7lMiVUDpmYvostbFzdLoVWsnjZ1veIa4ivo/vTLlU9YBB2PqciHDetKnNlRKpdMLE9Os0Js8jhMOUlKrlZkiSfWVR60ofgPlD4GvAo0ePHt1wA4erl1TqI9LaiM7iQeK2N7aD2nlqEycJwi71ernvnPemGWHX/DP4HoGPHj16dFNzDZsuA5MkQWs9PAkuHFJ1aGx7gcbUS72/eflWnOMyxoy6S/6P8F135x577LEt0SabPgONMRhjUEqV92TeOhfrYTxxZx577LEtrRfZ0t5Z/f2zim3EtUjTdFQ360Xgz4E/eOyxx0qbx24WtlwGdrtdut0u7Xaber1e2E45FwE+klx54vwJXin82Ve+MnoP1I1iQ/uFAY4cOTKDXy6/WnoxIYiiCCEEYRj2jO1cKfhGP8Nn6ioZ+BngoaNHj57a0ANsEBsmMEcVInMCh0FKSb3gO3EZgZ8BPnX06NET677ZTcSmEZjjyJEjGvgwvknFXSsutn4CTwCfMsb86Ze+9KWrvkyLsOkE5jhy5IgEdgD/Ht/t484RCTwFfBb4H8CJrdam68WWEdiPjMyPSCnfG4bhvx02LiPwT/FK4TPXK2n9uCoE9uOBBx6YxcvKX1r10+fwSuHlq3pDG8RVJzDHAw88sBcvJyfxxL14TW5kg/h/tOsouz7eRYQAAAAASUVORK5CYII=';
-// 플레이어 3D 스킨 컴포넌트 - 로딩 중에는 스티브, 로드 완료 시 실제 스킨 표시
+// 플레이어 3D 스킨 컴포넌트 - 스킨 캐싱 API 사용
const PlayerSkinImage = ({ uuid, playerName }) => {
const [src, setSrc] = useState(STEVE_BODY_BASE64);
useEffect(() => {
- const img = new Image();
- const realUrl = `https://mc-heads.net/body/${uuid}/80`;
- img.onload = () => setSrc(realUrl);
- img.src = realUrl;
+ // 스킨 캐싱 API 호출 (body/uuid/size)
+ fetch(`/link/skin/body/${uuid}/80`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.url) {
+ const img = new Image();
+ img.onload = () => setSrc(data.url);
+ img.onerror = () => setSrc(`https://mc-heads.net/body/${uuid}/80`);
+ img.src = data.url;
+ }
+ })
+ .catch(() => {
+ // 폴백: mc-heads 직접 사용
+ setSrc(`https://mc-heads.net/body/${uuid}/80`);
+ });
}, [uuid]);
return (
diff --git a/frontend/src/pages/PlayersPage.jsx b/frontend/src/pages/PlayersPage.jsx
index 7e6938e..635eae3 100644
--- a/frontend/src/pages/PlayersPage.jsx
+++ b/frontend/src/pages/PlayersPage.jsx
@@ -8,15 +8,26 @@ import { formatPlayTimeMs } from '../utils/formatters';
// 스티브 머리 기본 이미지 (로딩 전/실패 시 사용)
const STEVE_HEAD_BASE64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABWUlEQVRoge2ZPUsDQRCGk7DeGU1ysTDYpIuFjYUasMpHaZFKhJDWxj7gjwjYK/Y2wcJKsAppxEYkTcCU1oGcubt8gv6Bd4oBk3FhnvLZvb17GW7YvYsfF3Z+YgxSjuFMZxPMl9BPCZ9Y5cOsAw0gjQaQxvjBBA54qST0VJfgdqe/Wsf6CmgAaTSANMZ1NuDAVfkU+r3cLl5oKw39MhpDP5ktoPf9EfR33Tfora+ABpBGA0gTv788gycyz8vCC5Iu7loUVLfhQnUn6yugAaTRANIYqttc3DxAf15qQV8vfkBPdY/nzwr0j91r6NvNBvTWV0ADSKMBpGHvhSg6733W/MrRAWu+7oX+KxpAGusDmDCK4ECwCKF/ee1Bf5jfZ934tv0Efa16Av13iE921ldAA0ijAaQxxsU/6odj/NbPpnPoe18D1nx304Ge6jaZbfw9yvoKaABpNIA0v2NsVwyhlV0PAAAAAElFTkSuQmCC';
-// 플레이어 아바타 컴포넌트 - 로딩 중에는 스티브, 로드 완료 시 실제 스킨 표시
+// 플레이어 아바타 컴포넌트 - 스킨 캐싱 API 사용
const PlayerAvatar = ({ uuid, name }) => {
const [src, setSrc] = useState(STEVE_HEAD_BASE64);
useEffect(() => {
- const img = new Image();
- const realUrl = `https://mc-heads.net/avatar/${uuid}/48`;
- img.onload = () => setSrc(realUrl);
- img.src = realUrl;
+ // 스킨 캐싱 API 호출 (avatar/uuid/size)
+ fetch(`/link/skin/avatar/${uuid}/48`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.url) {
+ const img = new Image();
+ img.onload = () => setSrc(data.url);
+ img.onerror = () => setSrc(`https://mc-heads.net/avatar/${uuid}/48`);
+ img.src = data.url;
+ }
+ })
+ .catch(() => {
+ // 폴백: mc-heads 직접 사용
+ setSrc(`https://mc-heads.net/avatar/${uuid}/48`);
+ });
}, [uuid]);
return (
diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx
index e7bece3..1a856e0 100644
--- a/frontend/src/pages/ProfilePage.jsx
+++ b/frontend/src/pages/ProfilePage.jsx
@@ -4,14 +4,16 @@
*/
import React, { useState, useEffect } from 'react';
-import { Link, useNavigate } from 'react-router-dom';
+import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { ArrowLeft, Copy, Check, Gamepad2, Link as LinkIcon, Unlink, RefreshCw, User, Mail } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
+import { io } from 'socket.io-client';
export default function ProfilePage() {
const navigate = useNavigate();
- const { user, isLoggedIn, loading, checkAuth } = useAuth();
+ const location = useLocation();
+ const { user, isLoggedIn, loading, checkAuth, logout } = useAuth();
const [linkStatus, setLinkStatus] = useState(null);
const [linkToken, setLinkToken] = useState(null);
@@ -20,13 +22,14 @@ export default function ProfilePage() {
const [isLoading, setIsLoading] = useState(false);
const [polling, setPolling] = useState(false);
const [showUnlinkDialog, setShowUnlinkDialog] = useState(false);
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
// 로그인 체크 (loading 완료 후에만)
useEffect(() => {
if (!loading && !isLoggedIn) {
- navigate('/login');
+ navigate('/login', { state: { from: location.pathname } });
}
- }, [loading, isLoggedIn, navigate]);
+ }, [loading, isLoggedIn, navigate, location.pathname]);
// 연동 상태 확인
useEffect(() => {
@@ -69,6 +72,27 @@ export default function ProfilePage() {
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [linkToken]);
+ // 소켓으로 닉네임 변경 감지 시 동기화
+ useEffect(() => {
+ if (!linkStatus?.minecraftUuid) return;
+
+ const socket = io(window.location.origin, { path: '/socket.io' });
+
+ socket.on('status', async (status) => {
+ if (status?.online && status?.players?.list) {
+ const playerInGame = status.players.list.find(p => p.uuid === linkStatus.minecraftUuid);
+ // 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
+ if (playerInGame && playerInGame.name !== user?.name) {
+ // fetchLinkStatus가 DB 업데이트 → 완료 후 checkAuth로 user.name 갱신
+ await fetchLinkStatus();
+ await checkAuth();
+ }
+ }
+ });
+
+ return () => socket.disconnect();
+ }, [linkStatus?.minecraftUuid, user?.name]);
+
const fetchLinkStatus = async () => {
try {
const token = localStorage.getItem('token');
@@ -129,6 +153,25 @@ export default function ProfilePage() {
}
};
+ const handleDelete = async () => {
+ try {
+ const token = localStorage.getItem('token');
+ const res = await fetch('/auth/delete', {
+ method: 'DELETE',
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+
+ if (res.ok) {
+ logout();
+ navigate('/');
+ } else {
+ console.error('탈퇴 실패');
+ }
+ } catch (error) {
+ console.error('탈퇴 실패:', error);
+ }
+ };
+
if (!user) return null;
return (
@@ -265,6 +308,28 @@ export default function ProfilePage() {
)}
+
+ {/* 계정 관리 섹션 */}
+
+
+
+ 계정 관리
+
+
+
+
+
+ 회원 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다.
+
+
+
+
+
{/* 연동 해제 다이얼로그 */}
@@ -306,6 +371,47 @@ export default function ProfilePage() {
>
)}
+
+ {/* 탈퇴 확인 다이얼로그 */}
+
+ {showDeleteDialog && (
+ <>
+ setShowDeleteDialog(false)}
+ />
+
+ ⚠️ 회원 탈퇴
+
+ 정말 탈퇴하시겠습니까? 모든 데이터가 삭제되며 복구할 수 없습니다.
+
+
+
+
+
+
+ >
+ )}
+
);
}
+
diff --git a/frontend/src/pages/ServerDetail.jsx b/frontend/src/pages/ServerDetail.jsx
index cf9d558..b85d971 100644
--- a/frontend/src/pages/ServerDetail.jsx
+++ b/frontend/src/pages/ServerDetail.jsx
@@ -7,6 +7,34 @@ import Tooltip from '../components/Tooltip';
import { io } from 'socket.io-client';
+// 캐시된 스킨 컴포넌트 (helm 타입 지원)
+const CachedSkin = ({ uuid, name, size = 24 }) => {
+ const [src, setSrc] = useState(null);
+ const fallbackUrl = `https://mc-heads.net/avatar/${uuid}/${size}`;
+
+ useEffect(() => {
+ fetch(`/link/skin/avatar/${uuid}/${size}`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.url) {
+ setSrc(data.url);
+ } else {
+ setSrc(fallbackUrl);
+ }
+ })
+ .catch(() => setSrc(fallbackUrl));
+ }, [uuid, size, fallbackUrl]);
+
+ return (
+
{ e.target.src = fallbackUrl; }}
+ />
+ );
+};
+
const ServerDetail = ({ isMobile = false }) => {
const [server, setServer] = useState(null);
const [loading, setLoading] = useState(true);
@@ -45,7 +73,7 @@ const ServerDetail = ({ isMobile = false }) => {
});
socket.on('connect', () => {
- console.log('서버 소켓 연결 성공');
+ // 소켓 연결 성공
});
socket.on('status', (data) => {
@@ -53,7 +81,7 @@ const ServerDetail = ({ isMobile = false }) => {
});
socket.on('disconnect', () => {
- console.log('서버 소켓 연결 해제');
+ // 소켓 연결 해제
});
return () => {
@@ -246,15 +274,7 @@ const ServerDetail = ({ isMobile = false }) => {
className="flex items-center gap-2.5 px-4 py-2.5 rounded-lg bg-white/5 border border-white/10 hover:border-mc-green/50 hover:bg-mc-green/5 transition-all cursor-pointer group hover:scale-[1.03] active:scale-[0.98]"
>
-

{
- e.target.onerror = null;
- e.target.src = 'https://minotar.net/helm/Steve/24.png';
- }}
- />
+
{/* 온라인 표시 */}
diff --git a/frontend/src/pages/WorldsPage.jsx b/frontend/src/pages/WorldsPage.jsx
index 100d3a0..1c7a0b1 100644
--- a/frontend/src/pages/WorldsPage.jsx
+++ b/frontend/src/pages/WorldsPage.jsx
@@ -3,6 +3,34 @@ import { Globe, Sun, CloudRain, CloudLightning, Clock, Users, MapPin, ServerOff
import { motion } from 'framer-motion';
import { io } from 'socket.io-client';
+// 캐시된 스킨 컴포넌트
+const CachedSkin = ({ uuid, name, type = 'avatar', size = 40, className }) => {
+ const [src, setSrc] = useState(null);
+ const fallbackUrl = `https://mc-heads.net/${type}/${uuid}/${size}`;
+
+ useEffect(() => {
+ fetch(`/link/skin/${type}/${uuid}/${size}`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.url) {
+ setSrc(data.url);
+ } else {
+ setSrc(fallbackUrl);
+ }
+ })
+ .catch(() => setSrc(fallbackUrl));
+ }, [uuid, type, size, fallbackUrl]);
+
+ return (
+
{ e.target.src = fallbackUrl; }}
+ />
+ );
+};
+
// 월드 정보 페이지
const WorldsPage = ({ isMobile = false }) => {
const [worlds, setWorlds] = useState([]);
@@ -188,12 +216,11 @@ const WorldsPage = ({ isMobile = false }) => {
className={`flex items-center gap-3 bg-white/5 rounded-xl ${isMobile ? 'p-2' : 'p-3'}`}
>
{/* 모바일: 머리만, PC: 전신 */}
-