From 0b999f05268064e7585dc865afc22d3341907747 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Sun, 12 Apr 2026 19:51:55 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20pages/wallet=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=E2=80=94=20CSS=20=EB=B6=84=EB=A6=AC,=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WalletSummary.css 신규 분리 (WalletPage.css 결합도 제거) - InventoryTab: useCallback + useEffect([load]) 패턴으로 통일 - LoginPage: handleSSAFYLogin 함수 분리 - RegisterPage: 유효성 검사 메시지 세분화 (빈값/짧음/문자 오류 구분), 테스트 52개 전부 통과 - SSAFYCallbackPage: AuthPage.css 임포트 + 버튼 클래스 적용 - AuthPage.css: .login-logo 클래스 추가 - AdminPage.css: 후행 공백 제거 Co-Authored-By: Claude Sonnet 4.6 --- src/components/wallet/InventoryTab.jsx | 18 +++++---- src/components/wallet/WalletSummary.css | 51 +++++++++++++++++++++++++ src/components/wallet/WalletSummary.jsx | 2 +- src/pages/AdminPage.css | 2 +- src/pages/AuthPage.css | 7 ++++ src/pages/LoginPage.jsx | 24 ++++++------ src/pages/RegisterPage.jsx | 30 +++++++-------- src/pages/SSAFYCallbackPage.jsx | 5 ++- src/pages/WalletPage.css | 50 ------------------------ 9 files changed, 100 insertions(+), 89 deletions(-) create mode 100644 src/components/wallet/WalletSummary.css diff --git a/src/components/wallet/InventoryTab.jsx b/src/components/wallet/InventoryTab.jsx index 710c53f..c3eb772 100644 --- a/src/components/wallet/InventoryTab.jsx +++ b/src/components/wallet/InventoryTab.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { getInventory } from '../../api/chain'; import { useToast } from '../toast/useToast'; @@ -7,14 +7,16 @@ export default function InventoryTab() { const [inventory, setInventory] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - let cancelled = false; + const load = useCallback(() => { + setLoading(true); getInventory() - .then((data) => { if (!cancelled) setInventory(data); }) - .catch(() => { if (!cancelled) toast.error('인벤토리를 불러오지 못했습니다.'); }) - .finally(() => { if (!cancelled) setLoading(false); }); - return () => { cancelled = true; }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps + .then(setInventory) + .catch(() => toast.error('인벤토리를 불러오지 못했습니다.')) + .finally(() => setLoading(false)); + }, [toast]); + + // eslint-disable-next-line react-hooks/set-state-in-effect + useEffect(() => { load(); }, [load]); if (loading) return
불러오는 중...
; diff --git a/src/components/wallet/WalletSummary.css b/src/components/wallet/WalletSummary.css new file mode 100644 index 0000000..689c054 --- /dev/null +++ b/src/components/wallet/WalletSummary.css @@ -0,0 +1,51 @@ +.wallet-summary { + background: rgba(186, 205, 176, 0.06); + border: 1px solid rgba(186, 205, 176, 0.15); + border-radius: 10px; + padding: 20px 24px; + margin-bottom: 32px; + cursor: pointer; + transition: border-color 0.2s, background 0.2s; +} + +.wallet-summary:hover { + border-color: rgba(186, 205, 176, 0.3); + background: rgba(186, 205, 176, 0.09); +} + +.wallet-summary-title { + font-size: 0.75rem; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.4); + letter-spacing: 0.08em; + margin: 0 0 12px; +} + +.wallet-summary-balance { + font-size: 1.8rem; + font-weight: 700; + color: #BACDB0; + margin: 0; +} + +.wallet-summary-stats { + display: flex; + gap: 24px; + margin-top: 12px; +} + +.wallet-summary-stat { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); +} + +.wallet-summary-stat strong { + color: rgba(255, 255, 255, 0.8); +} + +@media (max-width: 768px) { + .wallet-summary-stats { + flex-direction: column; + gap: 8px; + } +} diff --git a/src/components/wallet/WalletSummary.jsx b/src/components/wallet/WalletSummary.jsx index 01d283c..ca4feca 100644 --- a/src/components/wallet/WalletSummary.jsx +++ b/src/components/wallet/WalletSummary.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { getBalance, getAssets, getInventory } from '../../api/chain'; -import '../../pages/WalletPage.css'; +import './WalletSummary.css'; export default function WalletSummary() { const navigate = useNavigate(); diff --git a/src/pages/AdminPage.css b/src/pages/AdminPage.css index 2e6ef46..e6146b9 100644 --- a/src/pages/AdminPage.css +++ b/src/pages/AdminPage.css @@ -147,4 +147,4 @@ .admin-main { padding: 20px 12px 60px; } -} +} \ No newline at end of file diff --git a/src/pages/AuthPage.css b/src/pages/AuthPage.css index a16860b..77cbe90 100644 --- a/src/pages/AuthPage.css +++ b/src/pages/AuthPage.css @@ -151,6 +151,13 @@ border-color: rgba(186, 205, 176, 0.5); } +.login-logo { + max-width: 120px; + height: auto; + margin: 0 auto 16px; + display: block; +} + .login-back { display: block; text-align: center; diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 8d4316a..5debda0 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -14,6 +14,18 @@ export default function LoginPage() { const location = useLocation(); const justRegistered = location.state?.registered; + const handleSSAFYLogin = async () => { + try { + const data = await getSSAFYLoginURL(); + if (!data.url || !data.url.startsWith('https://')) { + throw new Error('유효하지 않은 로그인 URL입니다.'); + } + window.location.href = data.url; + } catch { + setError('SSAFY 로그인 URL을 가져올 수 없습니다.'); + } + }; + const handleSubmit = async (e) => { e.preventDefault(); setError(''); @@ -84,17 +96,7 @@ export default function LoginPage() { diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx index 29c9fb9..29b13d4 100644 --- a/src/pages/RegisterPage.jsx +++ b/src/pages/RegisterPage.jsx @@ -5,6 +5,13 @@ import './AuthPage.css'; const USERNAME_REGEX = /^[a-z0-9_-]{3,50}$/; +function getPasswordStrength(pw) { + if (pw.length === 0) return { label: '', level: '' }; + if (pw.length < 6) return { label: '약함', level: 'weak' }; + if (pw.length < 10) return { label: '중간', level: 'medium' }; + return { label: '강함', level: 'strong' }; +} + export default function RegisterPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -18,13 +25,6 @@ export default function RegisterPage() { const showUsernameError = usernameTouched && username.length > 0 && !isUsernameValid; const showUsernameValid = usernameTouched && username.length > 0 && isUsernameValid; - const getPasswordStrength = (pw) => { - if (pw.length === 0) return { label: '', level: '' }; - if (pw.length < 6) return { label: '약함', level: 'weak' }; - if (pw.length < 10) return { label: '중간', level: 'medium' }; - return { label: '강함', level: 'strong' }; - }; - const passwordStrength = getPasswordStrength(password); const confirmMismatch = confirm.length > 0 && password !== confirm; const confirmMatch = confirm.length > 0 && password === confirm; @@ -34,7 +34,7 @@ export default function RegisterPage() { setError(''); const trimmed = username.trim().toLowerCase(); - if (!trimmed) { + if (trimmed.length === 0) { setError('아이디를 입력해주세요.'); return; } @@ -42,7 +42,7 @@ export default function RegisterPage() { setError('아이디는 3자 이상이어야 합니다.'); return; } - if (!/^[a-z0-9_-]+$/.test(trimmed)) { + if (!USERNAME_REGEX.test(trimmed)) { setError('아이디는 영문 소문자, 숫자, _, -만 사용 가능합니다.'); return; } @@ -70,7 +70,7 @@ export default function RegisterPage() {
- One of the Plans + One of the Plans

One of the plans

MULTIPLAYER

@@ -128,12 +128,10 @@ export default function RegisterPage() { className={confirmMatch ? 'input-valid' : confirmMismatch ? 'input-invalid' : ''} aria-describedby="confirm-hint" /> - {confirmMismatch && ( - 비밀번호가 일치하지 않습니다 - )} - {confirmMatch && ( - 비밀번호가 일치합니다 {'\u2713'} - )} + + {confirmMismatch && '비밀번호가 일치하지 않습니다'} + {confirmMatch && `비밀번호가 일치합니다 \u2713`} +
{error &&

{error}

} diff --git a/src/pages/SSAFYCallbackPage.jsx b/src/pages/SSAFYCallbackPage.jsx index f697dae..d693b5b 100644 --- a/src/pages/SSAFYCallbackPage.jsx +++ b/src/pages/SSAFYCallbackPage.jsx @@ -2,8 +2,9 @@ import { useEffect, useState, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAuth } from '../context/useAuth'; import { ssafyCallback } from '../api/auth'; +import './AuthPage.css'; -// Inline styles are intentional for this simple callback/loading page — not worth a separate CSS file. +// Inline styles are intentional for this simple callback/loading page — layout only, AuthPage.css handles buttons. export default function SSAFYCallbackPage() { const [error, setError] = useState(''); const navigate = useNavigate(); @@ -39,7 +40,7 @@ export default function SSAFYCallbackPage() { return (

{error}

-
diff --git a/src/pages/WalletPage.css b/src/pages/WalletPage.css index 22c06bd..74771a3 100644 --- a/src/pages/WalletPage.css +++ b/src/pages/WalletPage.css @@ -367,51 +367,6 @@ background: rgba(186, 205, 176, 0.08); } -/* Wallet summary card (HomePage) */ -.wallet-summary { - background: rgba(186, 205, 176, 0.06); - border: 1px solid rgba(186, 205, 176, 0.15); - border-radius: 10px; - padding: 20px 24px; - margin-bottom: 32px; - cursor: pointer; - transition: border-color 0.2s, background 0.2s; -} - -.wallet-summary:hover { - border-color: rgba(186, 205, 176, 0.3); - background: rgba(186, 205, 176, 0.09); -} - -.wallet-summary-title { - font-size: 0.75rem; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.4); - letter-spacing: 0.08em; - margin: 0 0 12px; -} - -.wallet-summary-balance { - font-size: 1.8rem; - font-weight: 700; - color: #BACDB0; - margin: 0; -} - -.wallet-summary-stats { - display: flex; - gap: 24px; - margin-top: 12px; -} - -.wallet-summary-stat { - font-size: 0.85rem; - color: rgba(255, 255, 255, 0.5); -} - -.wallet-summary-stat strong { - color: rgba(255, 255, 255, 0.8); -} .wallet-logo { height: 32px; @@ -470,11 +425,6 @@ align-items: stretch; } - .wallet-summary-stats { - flex-direction: column; - gap: 8px; - } - .market-item { flex-direction: column; align-items: stretch;