Files
2026-06-11 18:19:26 +09:00

692 lines
43 KiB
React
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable */
// ─────────────────────────────────────────────────────────────
// searchgg · 메인 홈 (레트로 구조형 · 올리브 포인트 · 각진 패널 레이아웃)
// 공지/업데이트/이벤트 전면 · 이벤트 카드 이미지 슬롯
// window.Home
// ─────────────────────────────────────────────────────────────
const { useState: useStateH } = React;
const PT = '#48515f'; // 올리브/카키 포인트
const PT_SOFT = 'rgba(72,81,95,.12)';
const HEAD = '#f1f3f5'; // 패널 헤더바 배경
const CH = "'Chakra Petch',sans-serif";
const panel = { border: '1px solid var(--line2)', background: '#fff', borderRadius: 5 };
// 패널 (채워진 헤더바 + 보더)
function Panel({ title, en, right, children, bodyPad = '8px 14px 12px' }) {
return (
<section style={panel}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 40, padding: '0 12px 0 0', background: HEAD, borderBottom: '1px solid var(--line2)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, minWidth: 0 }}>
<span style={{ width: 4, alignSelf: 'stretch', background: PT, margin: '8px 0', marginRight: 2 }} />
<span style={{ fontSize: 14.5, fontWeight: 800, color: 'var(--ink)', letterSpacing: '-.2px' }}>{title}</span>
{en && <span style={{ fontFamily: CH, fontSize: 10, fontWeight: 600, color: 'var(--t3)', letterSpacing: '1.2px' }}>{en}</span>}
</div>
{right}
</div>
<div style={{ padding: bodyPad }}>{children}</div>
</section>
);
}
// 세그먼트 탭 (각진)
function Seg({ tabs, value, onChange }) {
return (
<div style={{ display: 'flex', border: '1px solid var(--line2)', borderRadius: 4, overflow: 'hidden', background: '#fff' }}>
{tabs.map((t, i) => {
const on = value === t;
return (
<button key={t} onClick={() => onChange(t)} style={{ padding: '5px 11px', border: 'none', borderLeft: i ? '1px solid var(--line2)' : 'none', cursor: 'pointer',
fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, background: on ? PT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{t}</button>
);
})}
</div>
);
}
// ── 상단 유틸 바 ──
function TopBar() {
return (
<div style={{ height: 30, background: '#23262b', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', padding: '0 28px', gap: 0 }}>
{['로그인', '회원가입', '쿠폰등록', '고객센터'].map((t, i) => (
<React.Fragment key={t}>
{i > 0 && <span style={{ width: 1, height: 11, background: 'rgba(255,255,255,.18)', margin: '0 12px' }} />}
<a style={{ fontSize: 12, color: 'rgba(255,255,255,.72)', cursor: 'pointer', fontWeight: 500 }}>{t}</a>
</React.Fragment>
))}
</div>
);
}
// ── 메인 네비(GNB) ──
function Nav({ onLogin, onClan, onCommunity, active = '홈' }) {
const items = ['홈', '전적검색', '커뮤니티', '클랜'];
return (
<div style={{ background: '#fff', borderBottom: '2px solid #23262b', position: 'sticky', top: 0, zIndex: 30 }}>
<div style={{ maxWidth: 1180, margin: '0 auto', height: 60, display: 'flex', alignItems: 'center', padding: '0 28px' }}>
<a href="searchgg 메인 홈.html" style={{ textDecoration: 'none', display: 'flex' }}><Logo size={22} /></a>
<nav style={{ display: 'flex', marginLeft: 34, height: '100%' }}>
{items.map((t) => {
const on = t === active;
const style = { display: 'flex', alignItems: 'center', padding: '0 18px', height: '100%', fontSize: 15, fontWeight: on ? 800 : 600, textDecoration: 'none',
color: on ? PT : '#3a4049', cursor: 'pointer', borderBottom: on ? `3px solid ${PT}` : '3px solid transparent', background: 'transparent', border: 'none', borderBottomWidth: 3, borderBottomStyle: 'solid', borderBottomColor: on ? PT : 'transparent', fontFamily: "'Pretendard',sans-serif" };
if (t === '클랜' && onClan) {
return <button key={t} onClick={onClan} style={style}>{t}</button>;
}
if (t === '커뮤니티' && onCommunity) {
return <button key={t} onClick={onCommunity} style={style}>{t}</button>;
}
return (
<a key={t} href="searchgg 메인 홈.html" style={style}>{t}</a>
);
})}
</nav>
<button onClick={onLogin} style={{ marginLeft: 'auto', border: 'none', background: 'transparent', cursor: 'pointer',
color: '#3a4049', fontWeight: 700, fontSize: 14, padding: '6px 4px', fontFamily: "'Pretendard',sans-serif" }}>
로그인
</button>
</div>
</div>
);
}
// ── 검색 히어로(구조형 밴드) ──
function Hero({ onPick }) {
const SA = window.SA, me = SA.me;
return (
<div style={{ background: '#fff', borderBottom: '1px solid var(--line2)' }}>
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '34px 28px 30px' }}>
<div style={{ maxWidth: 720, margin: '0 auto', textAlign: 'center' }}>
<div style={{ fontFamily: CH, fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: PT, marginBottom: 10 }}>SUDDEN ATTACK RECORD SEARCH</div>
<h1 style={{ margin: '0 0 6px', fontWeight: 800, fontSize: 30, lineHeight: 1.2, letterSpacing: '-1px', color: 'var(--ink)' }}>전적 검색</h1>
<div style={{ fontSize: 13.5, color: 'var(--t2)', marginBottom: 18 }}>어서와요, <b style={{ color: PT }}>{me.name}</b> </div>
<SearchBar big onPick={onPick} />
<div style={{ marginTop: 13, display: 'flex', justifyContent: 'center' }}><RecentChips onPick={onPick} /></div>
</div>
</div>
</div>
);
}
// ── 섹션 헤더(채워진 바) ──
function BarHead({ title, en, more }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: '#23262b', color: '#fff', padding: '0 14px', height: 38, borderRadius: '5px 5px 0 0' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 9 }}>
<span style={{ fontSize: 14.5, fontWeight: 800, letterSpacing: '-.2px' }}>{title}</span>
{en && <span style={{ fontFamily: CH, fontSize: 10, fontWeight: 600, color: 'rgba(255,255,255,.5)', letterSpacing: '1.2px' }}>{en}</span>}
</div>
{more && <a style={{ fontSize: 12, color: 'rgba(255,255,255,.7)', cursor: 'pointer', fontWeight: 600 }}>전체보기 </a>}
</div>
);
}
// ── 진행중인 이벤트 (캐러셀) ──
function EventStrip() {
const SA = window.SA;
const PER = 4;
const pages = Math.ceil(SA.events.length / PER);
const [page, setPage] = useStateH(0);
const [hover, setHover] = useStateH(false);
React.useEffect(() => {
if (hover || pages <= 1) return;
const t = setInterval(() => setPage((p) => (p + 1) % pages), 5000);
return () => clearInterval(t);
}, [hover, pages]);
const go = (d) => setPage((p) => (p + d + pages) % pages);
const arrow = (dir) => (
<button onClick={() => go(dir === 'L' ? -1 : 1)} style={{ width: 26, height: 26, borderRadius: 4, border: 'none', background: 'rgba(255,255,255,.14)', color: '#fff', cursor: 'pointer', display: 'grid', placeItems: 'center', fontSize: 13 }}>
{dir === 'L' ? '' : ''}
</button>
);
return (
<div style={{ border: '1px solid var(--line2)', borderRadius: 5, marginBottom: 18, background: '#fff', overflow: 'hidden' }}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: '#23262b', color: '#fff', padding: '0 12px 0 14px', height: 38 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 9 }}>
<span style={{ fontSize: 14.5, fontWeight: 800, letterSpacing: '-.2px' }}>진행중인 이벤트</span>
<span style={{ fontFamily: CH, fontSize: 10, fontWeight: 600, color: 'rgba(255,255,255,.5)', letterSpacing: '1.2px' }}>ON-GOING EVENT</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontFamily: CH, fontSize: 11.5, fontWeight: 700, color: 'rgba(255,255,255,.7)' }}>{page + 1} / {pages}</span>
{arrow('L')}{arrow('R')}
</div>
</div>
<div style={{ overflow: 'hidden' }}>
<div style={{ display: 'flex', width: `${pages * 100}%`, transform: `translateX(-${page * (100 / pages)}%)`, transition: 'transform .45s cubic-bezier(.4,0,.2,1)' }}>
{Array.from({ length: pages }).map((_, pi) => (
<div key={pi} style={{ width: `${100 / pages}%`, display: 'grid', gridTemplateColumns: 'repeat(4,1fr)' }}>
{SA.events.slice(pi * PER, pi * PER + PER).map((e, i) => {
const gi = pi * PER + i;
const urgent = e.dday.startsWith('D-') && +e.dday.slice(2) <= 3;
return (
<div key={e.title} style={{ borderLeft: i ? '1px solid var(--line)' : 'none', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}>
<image-slot id={`evt-${gi}`} shape="rect" placeholder={`${e.tag} 배너 이미지를 끌어다 놓으세요`} style={{ display: 'block', width: '100%', height: 112, borderBottom: '1px solid var(--line)' }}></image-slot>
<div style={{ padding: '11px 13px 13px', display: 'flex', flexDirection: 'column', flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 8 }}>
<span style={{ fontFamily: "'Pretendard',sans-serif", fontSize: 11, fontWeight: 700, color: '#fff', background: PT, padding: '2px 9px', whiteSpace: 'nowrap', flexShrink: 0 }}>{e.tag}</span>
<span style={{ marginLeft: 'auto', fontFamily: CH, fontSize: 11.5, fontWeight: 700, color: urgent ? '#b23b3b' : PT }}>{e.dday}</span>
</div>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.34, marginBottom: 5 }}>{e.title}</div>
<div style={{ fontSize: 11.5, color: 'var(--t2)', lineHeight: 1.45, marginBottom: 'auto' }}>{e.sub}</div>
<div style={{ fontFamily: CH, fontSize: 11, color: 'var(--t3)', marginTop: 11, paddingTop: 9, borderTop: '1px solid var(--line)' }}>{e.period}</div>
</div>
</div>
);
})}
</div>
))}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, padding: '11px 0', borderTop: '1px solid var(--line)' }}>
{Array.from({ length: pages }).map((_, i) => (
<button key={i} onClick={() => setPage(i)} style={{ width: i === page ? 20 : 7, height: 7, borderRadius: 4, border: 'none', background: i === page ? PT : 'var(--line2)', cursor: 'pointer', transition: 'all .3s', padding: 0 }} />
))}
</div>
</div>
);
}
const rowDiv = (i, n) => ({ borderBottom: i < n - 1 ? '1px solid #eaecef' : 'none' });
// ── 지금 방송 중 LIVE (치지직 / 숲 / 유튜브) ──
function StreamLive({ onPick }) {
const SA = window.SA;
const [tab, setTab] = useStateH('전체');
const list = tab === '전체' ? SA.streams : SA.streams.filter((s) => s.platform === tab);
const totalView = SA.streams.reduce((a, s) => a + s.viewers, 0);
return (
<Panel title="서든 LIVE" en="STREAMING" bodyPad="12px 14px 14px"
right={<Seg tabs={['전체', '치지직', '숲', '유튜브']} value={tab} onChange={setTab} />}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 11, fontSize: 11.5, color: 'var(--t2)' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontWeight: 700, color: '#e5484d' }}>
<span style={{ width: 7, height: 7, borderRadius: 4, background: '#e5484d', display: 'inline-block', animation: 'sgFade .01s' }} />LIVE
</span>
<span>{list.length} 방송</span>
<span style={{ color: 'var(--t3)' }}>·</span>
<span>시청자 <b style={{ color: 'var(--ink)', fontFamily: CH }}>{SA.fmt(totalView)}</b></span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 12 }}>
{list.map((s, i) => {
const pf = SA.streamPlatforms[s.platform];
const player = SA.allPlayers.find((p) => p.name === s.streamer);
return (
<button key={s.streamer + i} onClick={() => player && onPick(player)} style={{ display: 'flex', flexDirection: 'column', textAlign: 'left',
border: '1px solid var(--line2)', borderRadius: 5, overflow: 'hidden', background: '#fff', cursor: player ? 'pointer' : 'default', padding: 0, fontFamily: "'Pretendard',sans-serif" }}>
<div style={{ position: 'relative' }}>
<image-slot id={`live-${i}`} shape="rect" placeholder={`${s.streamer} 방송 썸네일`} style={{ display: 'block', width: '100%', aspectRatio: '7 / 4' }}></image-slot>
<span style={{ position: 'absolute', top: 7, left: 7, display: 'inline-flex', alignItems: 'center', gap: 4, background: '#e5484d', color: '#fff', fontFamily: CH, fontWeight: 700, fontSize: 9.5, letterSpacing: '.6px', padding: '2px 6px', borderRadius: 3 }}>
<span style={{ width: 5, height: 5, borderRadius: 3, background: '#fff', display: 'inline-block' }} />LIVE
</span>
<span style={{ position: 'absolute', top: 7, right: 7, background: pf.color, color: '#fff', fontFamily: CH, fontWeight: 700, fontSize: 9.5, letterSpacing: '.5px', padding: '2px 7px', borderRadius: 3 }}>{pf.tag}</span>
<span style={{ position: 'absolute', bottom: 7, right: 7, background: 'rgba(20,22,28,.82)', color: '#fff', fontFamily: CH, fontWeight: 700, fontSize: 10.5, padding: '2px 7px', borderRadius: 3 }}>👁 {SA.fmt(s.viewers)}</span>
</div>
<div style={{ padding: '9px 10px 10px' }}>
<div style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.34, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', minHeight: 33 }}>{s.title}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 7 }}>
<span style={{ fontSize: 9, fontWeight: 800, color: pf.color, letterSpacing: '.3px' }}>{pf.label}</span>
<span style={{ width: 1, height: 9, background: 'var(--line2)' }} />
<span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--t2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.streamer}</span>
<span style={{ marginLeft: 'auto', fontSize: 10.5, color: 'var(--t3)', flexShrink: 0 }}>{s.started}</span>
</div>
</div>
</button>
);
})}
</div>
</Panel>
);
}
// ── SA 소식 ──
function SANews() {
const SA = window.SA;
const [tab, setTab] = useStateH('공지사항');
const rows = SA.news[tab];
return (
<Panel title="SA 소식" en="NOTICE" right={<Seg tabs={['공지사항', 'GM이야기', '업데이트']} value={tab} onChange={setTab} />} bodyPad="2px 14px 6px">
{rows.map((r, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 2px', ...rowDiv(i, rows.length), cursor: 'pointer' }}>
<span style={{ fontFamily: CH, fontSize: 12.5, color: 'var(--t3)', fontWeight: 600, width: 42, flexShrink: 0 }}>{r.date}</span>
<span style={{ flex: 1, fontSize: 14, color: '#33373d', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title}</span>
</div>
))}
</Panel>
);
}
// ── 커뮤니티 ──
function Community({ onAll }) {
const SA = window.SA;
const rows = [...SA.community['자유'], ...SA.community['공략']]
.sort((a, b) => (b.hot ? 1 : 0) - (a.hot ? 1 : 0))
.slice(0, 6);
return (
<Panel title="커뮤니티" en="COMMUNITY" right={
<button onClick={onAll} style={{ display: 'flex', alignItems: 'center', gap: 3, border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, color: 'var(--t2)', padding: 0 }}>
전체 보기 <Icon name="chevR" size={13} stroke="var(--t2)" />
</button>
} bodyPad="2px 14px 6px">
{rows.map((p, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 2px', ...rowDiv(i, rows.length), cursor: 'pointer' }}>
<span style={{ flexShrink: 0, fontSize: 10.5, fontWeight: 700, padding: '1px 7px', borderRadius: 4, background: '#f1f3f5', border: '1px solid var(--line)', color: '#5a5f66', minWidth: 32, textAlign: 'center' }}>{p.tag}</span>
<span style={{ flex: 1, fontSize: 14, color: '#33373d', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.title}</span>
{p.hot && <span style={{ fontSize: 10, fontWeight: 800, color: '#b23b3b', fontFamily: CH }}>HOT</span>}
<span style={{ fontFamily: CH, fontSize: 12, color: PT, fontWeight: 700, minWidth: 26, textAlign: 'right' }}>{p.cmt}</span>
<span style={{ fontSize: 11, color: 'var(--t3)', minWidth: 44, textAlign: 'right' }}>{p.time}</span>
</div>
))}
</Panel>
);
}
// ── 내 전적 요약 ──
function MeCard({ onPick }) {
const SA = window.SA, me = SA.me;
const [scope, setScope] = useStateH('통합');
const rankNo = scope === '통합' ? me.rankNoTotal : me.rankNoSeason;
const selRank = scope === '통합' ? me.rankTotal : me.rankSeason;
return (
<Panel title="내 전적" en="MY RECORD" bodyPad="14px 14px" right={<Seg tabs={['통합', '시즌']} value={scope} onChange={setScope} />}>
<div style={{ display: 'flex', alignItems: 'center', gap: 13 }}>
<RankBadge rank={selRank} size={46} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 7 }}>
<span style={{ fontSize: 16, fontWeight: 800, color: 'var(--ink)' }}>{me.name}</span>
<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>{me.clan}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 6 }}>
<span style={{ fontSize: 11.5, color: 'var(--t3)', fontWeight: 600 }}>{scope} 랭킹</span>
<span style={{ fontFamily: CH, fontSize: 15, fontWeight: 700, color: PT }}>{SA.fmt(rankNo)}<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 500, marginLeft: 1 }}></span></span>
</div>
</div>
</div>
<div style={{ display: 'flex', marginTop: 13, border: '1px solid var(--line)', borderRadius: 4 }}>
{[['승·패', `${me.wins}/${me.losses}`], ['승률', me.wr + '%'], ['킬뎃', SA.kdPct(me.kda) + '%']].map(([k, v], i) => (
<div key={k} style={{ flex: 1, textAlign: 'center', padding: '8px 0', borderLeft: i ? '1px solid var(--line)' : 'none', background: i % 2 ? '#fff' : '#f7f8f9' }}>
<div style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600, marginBottom: 3 }}>{k}</div>
<div style={{ fontFamily: CH, fontWeight: 700, fontSize: 15.5, color: k === '승률' ? wrColor(me.wr) : 'var(--ink)' }}>{v}</div>
</div>
))}
</div>
<button onClick={() => onPick(me)} style={{ width: '100%', marginTop: 12, padding: '9px 0', border: `1px solid ${PT}`, cursor: 'pointer',
background: '#fff', color: PT, fontWeight: 700, fontSize: 13, borderRadius: 4, fontFamily: "'Pretendard',sans-serif" }}> 전적 자세히 보기 </button>
</Panel>
);
}
// ── SA 랭킹 (5종 · 각 top10) ──
const SA_CATS = ['통합 계급', '시즌 계급', '랭크전', '공식 클랜', '클랜'];
function RankRow({ i, onClick, badge, name, tag, sub, value, unit }) {
return (
<button onClick={onClick} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', border: 'none', background: i % 2 ? '#fff' : '#f7f8f9', padding: '8px 14px', cursor: onClick ? 'pointer' : 'default', textAlign: 'left', borderBottom: '1px solid #eaecef', fontFamily: "'Pretendard',sans-serif" }}>
<span style={{ width: 16, textAlign: 'center', fontFamily: CH, fontWeight: 700, fontSize: 13.5, color: i < 3 ? PT : 'var(--t3)', flexShrink: 0 }}>{i + 1}</span>
{badge}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ fontSize: 13.5, color: 'var(--ink)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flexShrink: 0, maxWidth: '60%' }}>{name}</span>
{tag && <span style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{tag}</span>}
</div>
{sub && <div style={{ fontSize: 10.5, color: 'var(--t3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{sub}</div>}
</div>
<span style={{ fontFamily: CH, fontWeight: 700, fontSize: 13, color: 'var(--ink)', flexShrink: 0, whiteSpace: 'nowrap' }}>{value}{unit && <span style={{ fontSize: 10, color: 'var(--t3)', fontWeight: 500, marginLeft: 2 }}>{unit}</span>}</span>
</button>
);
}
// ── 즐겨찾기 (페이지네이션) ──
function Favorites({ onPick }) {
const SA = window.SA;
const all = SA.favorites;
const PER = 5;
const pages = Math.ceil(all.length / PER);
const [page, setPage] = useStateH(0);
const start = page * PER;
const view = all.slice(start, start + PER);
return (
<Panel title="즐겨찾기" en="FAVORITES" right={<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>{all.length}</span>} bodyPad="0">
<div>
{view.map((f, i) => (
<button key={f.name} onClick={() => onPick(f)} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', border: 'none', background: (start + i) % 2 ? '#fff' : '#f7f8f9', padding: '9px 14px', cursor: 'pointer', textAlign: 'left', borderBottom: '1px solid #eaecef', fontFamily: "'Pretendard',sans-serif" }}>
<RankBadge rank={f.rank} size={30} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ fontSize: 13.5, color: 'var(--ink)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flexShrink: 0, maxWidth: '60%' }}>{f.name}</span>
{f.clan && <span style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.clan}</span>}
</div>
<div style={{ fontSize: 10.5, color: 'var(--t3)', marginTop: 1 }}>{f.rank.name} · 승률 {f.wr}%</div>
</div>
<span style={{ fontFamily: CH, fontWeight: 700, fontSize: 12.5, color: PT, flexShrink: 0 }}>킬뎃 {SA.kdPct(f.kda)}%</span>
</button>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '10px 0' }}>
<button onClick={() => setPage(Math.max(0, page - 1))} disabled={page === 0} style={pgBtn(page === 0)}></button>
{Array.from({ length: pages }).map((_, i) => (
<button key={i} onClick={() => setPage(i)} style={{ width: 26, height: 26, borderRadius: 4, border: '1px solid ' + (i === page ? PT : 'var(--line2)'), background: i === page ? PT : '#fff', color: i === page ? '#fff' : 'var(--t2)', cursor: 'pointer', fontFamily: CH, fontWeight: 700, fontSize: 12 }}>{i + 1}</button>
))}
<button onClick={() => setPage(Math.min(pages - 1, page + 1))} disabled={page === pages - 1} style={pgBtn(page === pages - 1)}></button>
</div>
</Panel>
);
}
function pgBtn(disabled) {
return { width: 26, height: 26, borderRadius: 4, border: '1px solid var(--line2)', background: '#fff', color: disabled ? 'var(--line2)' : 'var(--t2)', cursor: disabled ? 'default' : 'pointer', fontFamily: "'Chakra Petch',sans-serif", fontWeight: 700, fontSize: 14 };
}
function SARanking({ onPick, onClan }) {
const SA = window.SA;
const [cat, setCat] = useStateH('통합 계급');
const [mode, setMode] = useStateH('솔로');
const tcol = Object.fromEntries(SA.tiers.map((t) => [t.name, t.color]));
const isClass = cat === '시즌 계급' || cat === '통합 계급';
const isRanked = cat === '랭크전';
const isRankedUser = isRanked && mode !== '클랜';
const isRankedClan = isRanked && mode === '클랜';
const isClan = cat === '공식 클랜' || cat === '클랜';
const classList = cat === '시즌 계급' ? SA.seasonClassRanking : SA.totalClassRanking;
const contentList = mode === '솔로' ? SA.soloRpRanking : SA.partyRpRanking;
const clanList = cat === '공식 클랜' ? SA.officialClanRanking : SA.generalClanRanking;
return (
<Panel title="SA 랭킹" en="RANKING" right={<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>TOP 10</span>} bodyPad="11px 0 0">
{/* 카테고리 선택 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, padding: '0 14px 10px' }}>
{SA_CATS.map((c) => {
const on = c === cat;
return (
<button key={c} onClick={() => setCat(c)} style={{ padding: '5px 10px', borderRadius: 4, border: `1px solid ${on ? PT : 'var(--line2)'}`, cursor: 'pointer',
fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 11.5, background: on ? PT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{c}</button>
);
})}
</div>
{/* 랭크전 하위 토글 */}
{isRanked && (
<div style={{ padding: '0 14px 11px' }}>
<div style={{ display: 'flex', border: '1px solid var(--line2)', borderRadius: 4, overflow: 'hidden', background: '#fff' }}>
{['솔로', '파티', '클랜'].map((t, i) => {
const on = mode === t;
return (
<button key={t} onClick={() => setMode(t)} style={{ flex: 1, padding: '6px 0', border: 'none', borderLeft: i ? '1px solid var(--line2)' : 'none', cursor: 'pointer',
fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, background: on ? PT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{t}</button>
);
})}
</div>
</div>
)}
{/* 계급 랭킹 */}
{isClass && classList.map((e, i) => {
const p = SA.allPlayers.find((x) => x.name === e.name);
return (
<RankRow key={e.name} i={i} onClick={() => p && onPick(p)}
badge={p ? <RankBadge rank={p.rank} size={28} /> : null}
tag={p && p.clan} name={e.name} sub={p && p.rank.name}
value={SA.fmt(e.score)} unit="점" />
);
})}
{/* 랭크전(솔로/파티) 랭킹 */}
{isRankedUser && contentList.map((e, i) => {
const p = SA.allPlayers.find((x) => x.name === e.name);
const col = tcol[e.tier] || PT;
return (
<RankRow key={e.name} i={i} onClick={() => p && onPick(p)}
badge={<div style={{ width: 28, height: 28, borderRadius: 4, background: col, display: 'grid', placeItems: 'center', color: '#fff', fontWeight: 800, fontSize: 12.5, flexShrink: 0 }}>{e.tier[0]}</div>}
tag={p && p.clan} name={e.name} sub={<span style={{ color: col, fontWeight: 600 }}>{e.tier}</span>}
value={SA.fmt(e.rp)} unit="RP" />
);
})}
{/* 랭크전(클랜) 랭킹 — 클랜 정보 */}
{isRankedClan && SA.clanRpRanking.map((c, i) => (
<RankRow key={c.name} i={i} onClick={() => onClan && onClan(c.name)}
badge={<div style={{ width: 28, height: 28, borderRadius: 4, background: '#23262b', display: 'grid', placeItems: 'center', fontFamily: CH, fontWeight: 700, fontSize: 10, color: '#fff', flexShrink: 0 }}>{c.tag}</div>}
name={c.name} sub={`${c.members}`} value={SA.fmt(c.rp)} unit="RP" />
))}
{/* 클랜 랭킹 */}
{isClan && clanList.map((c, i) => (
<RankRow key={c.name} i={i} onClick={() => onClan && onClan(c.name)}
badge={<div style={{ width: 28, height: 28, borderRadius: 4, background: '#23262b', display: 'grid', placeItems: 'center', fontFamily: CH, fontWeight: 700, fontSize: 10, color: '#fff', flexShrink: 0 }}>{c.tag}</div>}
name={<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>{c.name}{cat === '공식 클랜' && <span style={{ fontSize: 9, fontWeight: 700, color: PT, border: `1px solid ${PT}`, borderRadius: 3, padding: '0 4px', lineHeight: 1.5 }}>인증</span>}</span>}
sub={`${c.members}`} value={SA.fmt(c.rp)} unit="RP" />
))}
</Panel>
);
}
// ── 인기 검색 랭킹(가장 많이 조회된 유저) ──
function SearchRank({ onPick }) {
const SA = window.SA;
const trend = (m) => {
if (m === 'new') return <span style={{ fontFamily: CH, fontSize: 9.5, fontWeight: 700, color: '#b23b3b', width: 28, textAlign: 'center', flexShrink: 0 }}>NEW</span>;
if (m === 0) return <span style={{ fontSize: 11, color: 'var(--t3)', width: 28, textAlign: 'center', flexShrink: 0 }}></span>;
const up = m > 0;
return <span style={{ fontFamily: CH, fontSize: 11, fontWeight: 700, color: up ? PT : '#e5484d', width: 28, textAlign: 'center', flexShrink: 0 }}>{up ? '▲' : '▼'}{Math.abs(m)}</span>;
};
return (
<Panel title="인기 검색" en="TRENDING" right={<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>최근 24시간</span>} bodyPad="4px 0 0">
{SA.searchTrending.map((s, i) => {
const p = SA.allPlayers.find((x) => x.name === s.name);
return (
<button key={s.name} onClick={() => p && onPick(p)} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', border: 'none', background: i % 2 ? '#fff' : '#f7f8f9', padding: '8px 14px', cursor: p ? 'pointer' : 'default', textAlign: 'left', borderBottom: '1px solid #eaecef', fontFamily: "'Pretendard',sans-serif" }}>
<span style={{ width: 16, textAlign: 'center', fontFamily: CH, fontWeight: 700, fontSize: 13.5, color: i < 3 ? PT : 'var(--t3)' }}>{i + 1}</span>
{trend(s.move)}
<div style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 13.5, color: 'var(--ink)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flexShrink: 0, maxWidth: '60%' }}>{s.name}</span>
{p && p.clan && <span style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.clan}</span>}
</div>
<span style={{ fontFamily: CH, fontSize: 12, color: 'var(--t2)', fontWeight: 600, flexShrink: 0 }}>{SA.fmt(s.count)}<span style={{ fontSize: 10, color: 'var(--t3)', fontWeight: 500 }}> 조회</span></span>
</button>
);
})}
</Panel>
);
}
// ── 시즌 현황 ──
function SeasonCard() {
const SA = window.SA, s = SA.season;
const [mapTab, setMapTab] = useStateH('5vs5');
const maps = mapTab === '5vs5' ? s.maps5v5 : s.maps3v3;
return (
<Panel title="시즌 현황" en="SEASON" right={<span style={{ fontFamily: CH, fontSize: 12.5, fontWeight: 700, color: PT }}>{s.dday}</span>} bodyPad="14px 14px">
<div style={{ display: 'flex', alignItems: 'center', gap: 11, marginBottom: 13 }}>
<div style={{ width: 42, height: 42, borderRadius: 6, background: '#23262b', display: 'grid', placeItems: 'center', color: '#fff', fontFamily: CH, fontWeight: 700, fontSize: 14, flexShrink: 0 }}>S2</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 800, color: 'var(--ink)', lineHeight: 1.25 }}>{s.name}</div>
<div style={{ fontSize: 11.5, color: 'var(--t3)', marginTop: 2 }}>{s.period} 종료</div>
</div>
</div>
<div style={{ height: 7, background: '#d4d8de', borderRadius: 4, overflow: 'hidden', boxShadow: 'inset 0 0 0 1px rgba(0,0,0,.05)' }}>
<div style={{ width: s.progress + '%', height: '100%', background: PT, borderRadius: 4 }} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--t3)', marginTop: 5 }}>
<span>시즌 진행</span><span style={{ fontFamily: CH, color: 'var(--t2)', fontWeight: 600 }}>{s.progress}%</span>
</div>
<div style={{ marginTop: 14 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 7 }}>
<div style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600 }}>시즌 </div>
<div style={{ display: 'flex', border: '1px solid var(--line2)', borderRadius: 4, overflow: 'hidden', background: '#fff' }}>
{['5vs5', '3vs3'].map((t, i) => {
const on = mapTab === t;
return (
<button key={t} onClick={() => setMapTab(t)} style={{ padding: '3px 10px', border: 'none', borderLeft: i ? '1px solid var(--line2)' : 'none', cursor: 'pointer',
fontFamily: CH, fontWeight: 700, fontSize: 10.5, background: on ? PT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{t}</button>
);
})}
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{maps.map((m) => <span key={m} style={{ padding: '4px 10px', borderRadius: 4, background: '#fff', border: '1px solid var(--line2)', fontSize: 11.5, color: 'var(--t2)', fontWeight: 500 }}>{m}</span>)}
</div>
</div>
</Panel>
);
}
// ── 폼 좋은 유저 (연승 / 승률 / 킬뎃 탭) ──
function HotStreak({ onPick }) {
const SA = window.SA;
const [tab, setTab] = useStateH('연승');
let rows;
if (tab === '연승') {
rows = SA.hotStreak.map((e) => ({ p: SA.allPlayers.find((x) => x.name === e.name), streak: e.streak }));
} else if (tab === '승률') {
rows = [...SA.allPlayers].sort((a, b) => b.wr - a.wr).slice(0, 6).map((p) => ({ p }));
} else {
rows = [...SA.allPlayers].sort((a, b) => b.kda - a.kda).slice(0, 6).map((p) => ({ p }));
}
const metric = (p, streak) => {
if (tab === '연승') return (
<span style={{ display: 'flex', alignItems: 'center', gap: 3, color: '#e5484d' }}>
<Icon name="flame" size={14} stroke="#e5484d" />
<span style={{ fontFamily: CH, fontSize: 14, fontWeight: 700 }}>{streak}</span><span style={{ fontSize: 10.5, fontWeight: 700 }}>연승</span>
</span>
);
if (tab === '승률') return <span style={{ fontFamily: CH, fontSize: 15, fontWeight: 700, color: wrColor(p.wr) }}>{p.wr}%</span>;
return <span style={{ fontFamily: CH, fontSize: 15, fontWeight: 700, color: PT }}>{SA.kdPct(p.kda)}%</span>;
};
return (
<Panel title="폼 좋은 유저" en="HOT STREAK" right={<Seg tabs={['연승', '승률', '킬뎃']} value={tab} onChange={setTab} />} bodyPad="12px 14px 14px">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 10 }}>
{rows.map(({ p, streak }, i) => p && (
<button key={p.name} onClick={() => onPick(p)} style={{ display: 'flex', alignItems: 'center', gap: 10, border: '1px solid var(--line2)', borderRadius: 5, background: '#fff', padding: '10px 12px', cursor: 'pointer', textAlign: 'left', fontFamily: "'Pretendard',sans-serif" }}>
<span style={{ width: 14, textAlign: 'center', fontFamily: CH, fontWeight: 700, fontSize: 12.5, color: i < 3 ? PT : 'var(--t3)', flexShrink: 0 }}>{i + 1}</span>
<RankBadge rank={p.rank} size={30} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flexShrink: 0, maxWidth: '60%' }}>{p.name}</span>
{p.clan && <span style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.clan}</span>}
</div>
<div style={{ fontSize: 10.5, color: 'var(--t3)', marginTop: 1 }}>{p.rank.name}</div>
</div>
<div style={{ flexShrink: 0 }}>{metric(p, streak)}</div>
</button>
))}
</div>
</Panel>
);
}
// ── 오늘의 전장 ──
function LiveStats() {
const SA = window.SA, L = SA.liveStats;
return (
<Panel title="오늘의 전장" en="TODAY" bodyPad="14px 14px">
<div style={{ display: 'flex', alignItems: 'baseline', gap: 7, marginBottom: 12, whiteSpace: 'nowrap' }}>
<span style={{ fontFamily: CH, fontWeight: 700, fontSize: 26, color: 'var(--ink)', letterSpacing: '-.5px' }}>{SA.fmt(L.totalMatches)}</span>
<span style={{ fontSize: 12.5, color: 'var(--t2)' }}>매치</span>
<span style={{ marginLeft: 'auto', fontFamily: CH, fontSize: 12, color: PT, fontWeight: 700 }}> {L.prevDelta}%</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', borderTop: '1px solid var(--line)' }}>
{L.matchModes.map(({ name, count }) => (
<div key={name} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '8px 0', borderBottom: '1px solid #eaecef' }}>
<span style={{ fontSize: 12.5, color: 'var(--t2)' }}>{name}</span>
<span style={{ fontFamily: CH, fontWeight: 600, fontSize: 13, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{SA.fmt(count)}</span>
</div>
))}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '8px 0' }}>
<span style={{ fontSize: 12.5, color: 'var(--t2)' }}>오늘 제재</span>
<span style={{ fontFamily: CH, fontWeight: 600, fontSize: 13, color: '#b23b3b', whiteSpace: 'nowrap' }}>{L.bannedToday}</span>
</div>
</div>
</Panel>
);
}
function Footer() {
return (
<div style={{ borderTop: '1px solid var(--line2)', background: '#fff' }}>
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '24px 28px 34px', display: 'flex', alignItems: 'flex-start', gap: 20 }}>
<Logo size={18} />
<div style={{ display: 'flex', gap: 16, marginLeft: 8 }}>
{['이용약관', '개인정보처리방침', '공지사항', '문의하기'].map((t) => (
<a key={t} onClick={() => { if (t === '문의하기') window.dispatchEvent(new Event('sa-open-feedback')); }} style={{ fontSize: 12.5, color: 'var(--t2)', cursor: 'pointer' }}>{t}</a>
))}
</div>
<div style={{ marginLeft: 'auto', textAlign: 'right', fontSize: 11.5, color: 'var(--t3)', lineHeight: 1.7 }}>
searchgg는 서든어택 전적 조회를 위한 비공식 사이트입니다. 페이지의 데이터는 데모용 예시입니다.<br />© 2026 searchgg
</div>
</div>
</div>
);
}
// ── 모집 중인 클랜 (포스터) ──
function RecruitPosters({ onPlaza, onOpenClan }) {
let pool = window.ClanRecruits ? window.ClanRecruits() : [];
try { if (localStorage.getItem('sa-my-recruit-active') === '0') pool = pool.filter((c) => c.name !== (window.SA.me.clan || '')); } catch (e) {}
const list = pool.slice().sort((a, b) => b.hot - a.hot).slice(0, 4);
if (!list.length) return null;
return (
<Panel title="모집 중인 클랜" en="RECRUIT" right={
<button onClick={onPlaza} style={{ display: 'flex', alignItems: 'center', gap: 3, border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, color: 'var(--t2)', padding: 0 }}>
전체 보기 <Icon name="chevR" size={13} stroke="var(--t2)" />
</button>
} bodyPad="14px 14px">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12 }}>
{list.map((rc) => (
<div key={rc.name} onClick={onPlaza} style={{ cursor: 'pointer' }}>
<div style={{ aspectRatio: '3 / 4', border: '1px solid var(--line)', borderRadius: 5, overflow: 'hidden', background: '#f4f5f7' }}>
<image-slot id={`recruit-poster-${rc.name}`} shape="rect" placeholder="홍보 포스터" style={{ display: 'block', width: '100%', height: '100%' }}></image-slot>
</div>
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', gap: 7 }}>
<window.Emblem cfg={rc.emblem} size={20} />
<span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{rc.name}</span>
</div>
<div style={{ marginTop: 2, fontSize: 10.5, color: 'var(--t3)', paddingLeft: 27 }}>랭킹 {rc.rank} · {rc.openings} 모집</div>
</div>
))}
</div>
</Panel>
);
}
function Home() {
const [sel, setSel] = useStateH(null);
const [login, setLogin] = useStateH(false);
const [clan, setClan] = useStateH(null);
const [plaza, setPlaza] = useStateH(false);
const [community, setCommunity] = useStateH(false);
return (
<div style={{ minHeight: '100vh', background: 'var(--bg2)', color: 'var(--ink)', fontFamily: "'Pretendard',sans-serif" }}>
<Nav active="홈" onLogin={() => setLogin(true)} onClan={() => setPlaza(true)} onCommunity={() => setCommunity(true)} />
<Hero onPick={setSel} />
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '20px 28px 40px' }}>
<EventStrip />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 348px', gap: 18, alignItems: 'start' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
<StreamLive onPick={setSel} />
<HotStreak onPick={setSel} />
<RecruitPosters onPlaza={() => setPlaza(true)} onOpenClan={(n) => setClan(n)} />
<SANews />
<Community onAll={() => setCommunity(true)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
<MeCard onPick={setSel} />
<Favorites onPick={setSel} />
<SeasonCard />
<SARanking onPick={setSel} onClan={setClan} />
<SearchRank onPick={setSel} />
<LiveStats />
</div>
</div>
</div>
<Footer />
{sel && <ProfileScreen player={sel} onClose={() => setSel(null)} onPick={setSel} onLogin={() => setLogin(true)} onClan={(n) => { setSel(null); setClan(n); }} onCommunity={() => { setSel(null); setCommunity(true); }} />}
{plaza && <ClanPlaza onClose={() => setPlaza(false)} onOpenClan={(n) => setClan(n)} onLogin={() => setLogin(true)} onClan={() => setPlaza(true)} onCommunity={() => { setPlaza(false); setCommunity(true); }} />}
{community && <CommunityScreen onClose={() => setCommunity(false)} onLogin={() => setLogin(true)} onClan={() => { setCommunity(false); setPlaza(true); }} />}
{clan && <ClanHome clanName={clan} onClose={() => setClan(null)} onPick={(n) => { setClan(null); setSel(window.SA.allPlayers.find((p) => p.name === n) || window.SA.me); }} onLogin={() => setLogin(true)} onClan={() => { setClan(null); setPlaza(true); }} onCommunity={() => { setClan(null); setPlaza(false); setCommunity(true); }} />}
{login && <LoginScreen onClose={() => setLogin(false)} />}
<FeedbackWidget />
</div>
);
}
Object.assign(window, { Home, TopBar, Nav, Footer, Panel, Seg, PT, CH });