feat: 웹 클라이언트 비주얼 오버홀 — 게임 에셋 적용
All checks were successful
Client CI/CD / test (push) Successful in 9s
Client CI/CD / deploy (push) Successful in 14s

- 홈페이지: 드래곤 히어로 배경(bg_loading), fixed 헤더, 로고, 마을 배경 다운로드 섹션
- 로그인/회원가입: 고딕 성문 배경(bg_login), 카드 프레임, 게임 버튼
- 공통: btn-game, card-game, divider-game CSS 클래스 추가
- 게임 에셋 PNG → WebP 변환 (6MB → 80~234KB)
- 관리자 페이지: 로고/divider 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 01:55:16 +09:00
parent 7e7b3e85a7
commit b9bdbcaabc
20 changed files with 239 additions and 54 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.superpowers/

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
public/images/bg_login.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
public/images/bg_main.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/images/divider.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/images/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -1,5 +1,6 @@
.announcement-board {
margin-top: 32px;
padding: 24px;
}
.announcement-heading {
@@ -7,8 +8,9 @@
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
margin: 0 0 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(186, 205, 176, 0.15);
padding-bottom: 0;
border-bottom: none;
text-align: center;
}
.announcement-list {

View File

@@ -1,9 +1,12 @@
.download-section {
background: rgba(186, 205, 176, 0.06);
border: 1px solid rgba(186, 205, 176, 0.12);
position: relative;
background:
linear-gradient(to bottom, rgba(0,0,0,0.7), rgba(0,0,0,0.6)),
url('/images/bg_main.webp') center/cover no-repeat;
border-radius: 12px;
padding: 48px 40px;
padding: 60px 40px;
text-align: center;
overflow: hidden;
}
.download-title {

View File

@@ -30,4 +30,52 @@ a {
a:hover {
opacity: 0.85;
}
/* ── Game asset common classes ───────────────────── */
.btn-game {
position: relative;
border: none;
color: #fff;
padding: 14px 40px;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
background: url('/images/btn_normal.webp') center/100% 100% no-repeat;
letter-spacing: 0.05em;
overflow: hidden;
transition: transform 0.15s, brightness 0.15s;
}
.btn-game:hover { transform: translateY(-1px); filter: brightness(1.2); }
.btn-game:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-game > * { position: relative; z-index: 1; }
.card-game {
position: relative;
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
overflow: hidden;
}
.card-game::before {
content: '';
position: absolute;
inset: -8px;
background: url('/images/card_frame.webp') center/100% 100% no-repeat;
pointer-events: none;
z-index: 0;
}
.card-game > * { position: relative; z-index: 1; }
.divider-game {
width: 100%;
max-width: 400px;
height: 20px;
background: url('/images/divider.webp') center/contain no-repeat;
margin: 2rem auto;
}
@media (max-width: 768px) {
.card-game::before { display: none; }
.card-game { border: 1px solid rgba(186, 205, 176, 0.3); }
}

View File

@@ -96,6 +96,19 @@
padding: 32px 24px 80px;
}
.admin-logo {
height: 32px;
width: auto;
}
.admin-tabs-divider {
width: 100%;
max-width: 300px;
height: 16px;
background: url('/images/divider.webp') center/contain no-repeat;
margin: 0 auto;
}
/* Mobile responsive */
@media (max-width: 768px) {
.admin-header {

View File

@@ -21,6 +21,7 @@ export default function AdminPage() {
<header className="admin-header">
<div className="admin-header-left">
<Link to="/" className="admin-home-link"> 메인으로</Link>
<img src="/images/logo.webp" alt="" className="admin-logo" />
<h1 className="admin-title">관리자 페이지</h1>
</div>
<div className="admin-header-right">
@@ -44,6 +45,7 @@ export default function AdminPage() {
</button>
))}
</div>
<div className="admin-tabs-divider"></div>
{/* Tabs are conditionally rendered (not hidden) to avoid stale data. Each panel re-fetches on mount. */}
<main className="admin-main" role="tabpanel" id={`tabpanel-${tab}`} aria-labelledby={`tab-${tab}`}>

View File

@@ -3,20 +3,36 @@
display: flex;
align-items: center;
justify-content: center;
background-color: #2E2C2F;
background-image:
radial-gradient(ellipse at 20% 50%, rgba(186, 205, 176, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 50%, rgba(186, 205, 176, 0.05) 0%, transparent 50%);
background:
radial-gradient(ellipse at center, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.8) 100%),
url('/images/bg_login.webp') center/cover no-repeat;
background-color: #1a1a1a;
}
.login-panel {
position: relative;
width: 100%;
max-width: 400px;
max-width: 420px;
padding: 48px 40px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(186, 205, 176, 0.15);
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
backdrop-filter: blur(10px);
backdrop-filter: blur(12px);
overflow: hidden;
border: none;
}
.login-panel::before {
content: '';
position: absolute;
inset: -8px;
background: url('/images/card_frame.webp') center/100% 100% no-repeat;
pointer-events: none;
z-index: 0;
}
.login-panel > * {
position: relative;
z-index: 1;
}
.login-header {
@@ -116,21 +132,15 @@
.login-divider {
display: flex;
align-items: center;
gap: 16px;
justify-content: center;
margin: 24px 0;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: rgba(255, 255, 255, 0.1);
}
.login-divider span {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.35);
.login-divider-img {
width: 100%;
max-width: 250px;
height: 16px;
background: url('/images/divider.webp') center/contain no-repeat;
}
.btn-ssafy {
@@ -212,6 +222,11 @@
padding: 32px 20px;
margin: 0 12px;
border-radius: 8px;
border: 1px solid rgba(186, 205, 176, 0.3);
}
.login-panel::before {
display: none;
}
.game-title {

View File

@@ -3,21 +3,44 @@
background-color: #2E2C2F;
}
/* Header */
/* Header - fixed with scroll effect */
.home-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
background: transparent;
transition: background 0.3s;
}
.home-header.scrolled {
background: rgba(46, 44, 47, 0.85);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(186, 205, 176, 0.1);
}
.home-logo {
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.home-logo img {
height: 40px;
width: auto;
}
.home-logo-text {
font-size: 1.2rem;
font-weight: 800;
color: #BACDB0;
letter-spacing: 0.1em;
margin: 0;
}
.home-user {
@@ -77,13 +100,11 @@
opacity: 0.9;
}
/* Hero banner */
/* Hero banner - fullscreen with game background */
.hero-banner {
position: relative;
height: 280px;
background:
linear-gradient(135deg, rgba(46, 44, 47, 0.85), rgba(46, 44, 47, 0.6)),
linear-gradient(135deg, #2E2C2F 0%, #3a3a3a 50%, #2E2C2F 100%);
min-height: 100vh;
background: url('/images/bg_loading.webp') center/cover no-repeat;
display: flex;
align-items: center;
justify-content: center;
@@ -94,28 +115,62 @@
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 30% 40%, rgba(186, 205, 176, 0.12) 0%, transparent 60%),
radial-gradient(ellipse at 70% 60%, rgba(186, 205, 176, 0.06) 0%, transparent 50%);
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.7) 0%,
rgba(0, 0, 0, 0.3) 50%,
rgba(0, 0, 0, 0.8) 100%
);
}
.hero-overlay {
position: relative;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.hero-logo {
width: 200px;
height: auto;
margin-bottom: 24px;
}
.hero-title {
font-size: 2.8rem;
font-size: 3rem;
font-weight: 800;
color: #BACDB0;
letter-spacing: 0.12em;
letter-spacing: 0.15em;
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
margin: 0;
}
.hero-desc {
font-size: 1rem;
color: rgba(255, 255, 255, 0.5);
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
letter-spacing: 0.3em;
margin: 12px 0 0;
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.5);
}
.hero-cta {
margin-top: 40px;
}
.hero-scroll-hint {
position: absolute;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.4);
font-size: 0.8rem;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-8px); }
}
/* Main content */
@@ -128,19 +183,23 @@
/* Mobile responsive */
@media (max-width: 768px) {
.home-header {
flex-direction: column;
gap: 12px;
padding: 12px 16px;
text-align: center;
}
.home-header.scrolled {
padding: 8px 16px;
}
.home-user {
width: 100%;
justify-content: center;
gap: 8px;
}
.hero-banner {
height: 180px;
min-height: 80vh;
}
.hero-logo {
width: 120px;
}
.hero-title {
@@ -149,6 +208,7 @@
.hero-desc {
font-size: 0.85rem;
letter-spacing: 0.15em;
}
.home-main {

View File

@@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/useAuth';
import DownloadSection from '../components/DownloadSection';
@@ -7,11 +8,21 @@ import './HomePage.css';
export default function HomePage() {
const { user, logout } = useAuth();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div className="home-page">
<header className="home-header">
<h1 className="home-logo">One of the plans</h1>
<header className={`home-header${scrolled ? ' scrolled' : ''}`}>
<Link to="/" className="home-logo">
<img src="/images/logo.webp" alt="One of the Plans" />
<span className="home-logo-text">One of the Plans</span>
</Link>
<div className="home-user">
{user ? (
<>
@@ -30,14 +41,29 @@ export default function HomePage() {
<section className="hero-banner">
<div className="hero-overlay">
<h2 className="hero-title">One of the plans</h2>
<p className="hero-desc">Unity 3D 멀티플레이어 테스트에 참여하세요</p>
<img src="/images/logo.webp" alt="" className="hero-logo" />
<h2 className="hero-title">One of the Plans</h2>
<p className="hero-desc">MULTIPLAYER BOSS RAID</p>
<div className="hero-cta">
{user ? (
<a href="#download" className="btn-game" style={{display: 'inline-block', textDecoration: 'none'}}>
<span>게임 시작</span>
</a>
) : (
<Link to="/login" className="btn-game" style={{display: 'inline-block', textDecoration: 'none'}}>
<span>로그인</span>
</Link>
)}
</div>
</div>
<div className="hero-scroll-hint"> 스크롤</div>
</section>
<main className="home-main">
<main className="home-main" id="download">
<div className="divider-game"></div>
{user && <WalletSummary />}
<DownloadSection />
<div className="divider-game"></div>
<AnnouncementBoard />
</main>
</div>

View File

@@ -36,6 +36,7 @@ export default function LoginPage() {
<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'}} />
<h1 className="game-title">One of the plans</h1>
<p className="game-subtitle">MULTIPLAYER</p>
</div>
@@ -72,13 +73,13 @@ export default function LoginPage() {
{justRegistered && <p className="login-success">회원가입이 완료되었습니다. 로그인해주세요.</p>}
{error && <p id="login-error" className="login-error" role="alert">{error}</p>}
<button type="submit" className="btn-login" disabled={loading}>
<button type="submit" className="btn-login btn-game" disabled={loading}>
{loading ? '로그인 중...' : '로그인'}
</button>
</form>
<div className="login-divider">
<span>또는</span>
<div className="login-divider-img"></div>
</div>
<button

View File

@@ -70,6 +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'}} />
<h1 className="game-title">One of the plans</h1>
<p className="game-subtitle">MULTIPLAYER</p>
</div>
@@ -137,7 +138,7 @@ export default function RegisterPage() {
{error && <p className="login-error" role="alert">{error}</p>}
<button type="submit" className="btn-login" disabled={loading}>
<button type="submit" className="btn-login btn-game" disabled={loading}>
{loading ? '처리 중...' : '회원가입'}
</button>
</form>

View File

@@ -413,6 +413,19 @@
color: rgba(255, 255, 255, 0.8);
}
.wallet-logo {
height: 32px;
width: auto;
}
.wallet-tabs-divider {
width: 100%;
max-width: 300px;
height: 16px;
background: url('/images/divider.webp') center/contain no-repeat;
margin: 0 auto;
}
/* Mobile responsive */
@media (max-width: 768px) {
.wallet-header {