refactor: pages/wallet 리팩토링 — CSS 분리, 유효성 검사 개선, 테스트 통과
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <div className="wallet-spinner">불러오는 중...</div>;
|
||||
|
||||
|
||||
51
src/components/wallet/WalletSummary.css
Normal file
51
src/components/wallet/WalletSummary.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -147,4 +147,4 @@
|
||||
.admin-main {
|
||||
padding: 20px 12px 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn-ssafy"
|
||||
onClick={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을 가져올 수 없습니다.');
|
||||
}
|
||||
}}
|
||||
onClick={handleSSAFYLogin}
|
||||
>
|
||||
SSAFY 계정으로 로그인
|
||||
</button>
|
||||
|
||||
@@ -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() {
|
||||
<div className="login-page">
|
||||
<div className="login-panel">
|
||||
<div className="login-header">
|
||||
<img src="/images/logo.webp" alt="One of the Plans" style={{maxWidth: 120, height: 'auto', margin: '0 auto 16px', display: 'block'}} />
|
||||
<img src="/images/logo.webp" alt="One of the Plans" className="login-logo" />
|
||||
<h1 className="game-title">One of the plans</h1>
|
||||
<p className="game-subtitle">MULTIPLAYER</p>
|
||||
</div>
|
||||
@@ -128,12 +128,10 @@ export default function RegisterPage() {
|
||||
className={confirmMatch ? 'input-valid' : confirmMismatch ? 'input-invalid' : ''}
|
||||
aria-describedby="confirm-hint"
|
||||
/>
|
||||
{confirmMismatch && (
|
||||
<span id="confirm-hint" className="input-hint input-hint-error">비밀번호가 일치하지 않습니다</span>
|
||||
)}
|
||||
{confirmMatch && (
|
||||
<span id="confirm-hint" className="input-hint input-hint-success">비밀번호가 일치합니다 {'\u2713'}</span>
|
||||
)}
|
||||
<span id="confirm-hint" className={`input-hint ${confirmMismatch ? 'input-hint-error' : confirmMatch ? 'input-hint-success' : ''}`}>
|
||||
{confirmMismatch && '비밀번호가 일치하지 않습니다'}
|
||||
{confirmMatch && `비밀번호가 일치합니다 \u2713`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{error && <p className="login-error" role="alert">{error}</p>}
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ textAlign: 'center', marginTop: '4rem' }}>
|
||||
<p style={{ color: '#e74c3c' }}>{error}</p>
|
||||
<button onClick={() => navigate('/login', { replace: true })}>
|
||||
<button className="btn-login" onClick={() => navigate('/login', { replace: true })}>
|
||||
로그인 페이지로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user