Files
surinz/surinz-source/sa-profile-sections.jsx
T
2026-06-11 18:19:26 +09:00

527 lines
31 KiB
React

/* eslint-disable */
// ─────────────────────────────────────────────────────────────
// searchgg · 전적검색(프로필) 섹션 컴포넌트
// 요약 헤더 / 시즌별 랭크전 / 클랜전 / 맵별 / 최근 동향 / 매치 기록 / 자주 함께한 유저
// 전역(window): Panel, Seg, RankBadge, WinRing, Icon, KdaChip, wrColor, SA
// ─────────────────────────────────────────────────────────────
const PSCH = "'Chakra Petch',sans-serif";
const PSPT = '#48515f';
const psTh = (a) => ({ fontSize: 11, fontWeight: 700, color: 'var(--t3)', padding: '10px 14px', textAlign: a, whiteSpace: 'nowrap', letterSpacing: '.2px' });
const psTd = { fontSize: 13, color: '#33373d', padding: '11px 14px', whiteSpace: 'nowrap', verticalAlign: 'middle' };
const psSmall = { fontSize: 11, color: 'var(--t3)', fontWeight: 600 };
function tierColor(name) { const t = (window.SA.tiers || []).find((x) => x.name === name); return t ? t.color : PSPT; }
function recStr(d) { return `${d.matches}${d.w}${d.l}`; }
function WrCell({ wr, w = 46 }) {
const fill = Math.max(0.04, Math.min(1, (wr - 35) / (80 - 35)));
const col = wrColor(wr);
return (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, justifyContent: 'flex-end' }}>
<span style={{ width: w, height: 7, background: '#d4d8de', borderRadius: 4, overflow: 'hidden', display: 'inline-block', boxShadow: 'inset 0 0 0 1px rgba(0,0,0,.05)' }}>
<span style={{ display: 'block', width: `${fill * 100}%`, height: '100%', background: col }} />
</span>
<span style={{ fontFamily: PSCH, fontWeight: 700, color: col, minWidth: 36, textAlign: 'right' }}>{wr}%</span>
</div>
);
}
function TierChipS({ name, div }) {
const col = tierColor(name);
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, whiteSpace: 'nowrap' }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: col, transform: 'rotate(45deg)', flexShrink: 0 }} />
<span style={{ fontWeight: 700, color: 'var(--ink)' }}>{name}{div ? ' ' + div : ''}</span>
</span>
);
}
// ── 요약 헤더 ──
function ProfileSummary({ p }) {
const SA = window.SA;
const accent = p.rank.color;
const [scope, setScope] = React.useState('통합');
const selRank = scope === '통합' ? p.rankTotal : p.rankSeason;
const selRankNo = scope === '통합' ? p.rankNoTotal : p.rankNoSeason;
const stats = [
['승률', p.wr + '%', wrColor(p.wr)],
['킬뎃', SA.kdPct(p.kda) + '%', PSPT],
['헤드샷', p.hs + '%', 'var(--ink)'],
['라이플', p.rifle + '%', 'var(--ink)'],
['스나', p.sniper + '%', 'var(--ink)'],
['특수', p.special + '%', 'var(--ink)'],
['랭킹', SA.fmt(selRankNo) + '위', PSPT],
['매너', p.manner + '점', 'var(--ink)'],
['접속률', p.connRate + '%', 'var(--ink)'],
];
return (
<div style={{ background: '#fff', border: '1px solid var(--line)', borderRadius: 5, overflow: 'hidden', marginBottom: 18 }}>
<div style={{ height: 4, background: accent }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '22px 26px', flexWrap: 'wrap' }}>
<div style={{ textAlign: 'center', flexShrink: 0 }}>
<Seg tabs={['통합', '시즌']} value={scope} onChange={setScope} />
<div style={{ marginTop: 12, display: 'flex', justifyContent: 'center' }}><RankBadge rank={selRank} size={66} /></div>
<div style={{ fontSize: 12, color: selRank.color, fontWeight: 700, marginTop: 5 }}>{selRank.name}</div>
<div style={{ fontSize: 10, color: 'var(--t3)', marginTop: 1 }}>{scope} 계급</div>
</div>
<div style={{ flex: 1, minWidth: 220 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<h1 style={{ margin: 0, fontSize: 28, fontWeight: 800, color: 'var(--ink)', letterSpacing: '-.5px' }}>{p.name}</h1>
{p.clan && <span style={{ fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 14, color: 'var(--t2)' }}>{p.clan}</span>}
</div>
<div style={{ marginTop: 13, display: 'flex', gap: 8 }}>
<button style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', borderRadius: 5, border: 'none', background: '#20242b', color: '#fff', fontWeight: 700, fontSize: 12.5, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>
<Icon name="heart" size={14} stroke="#fff" /> 즐겨찾기
</button>
<button style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', borderRadius: 5, cursor: 'pointer', background: '#fff', border: '1px solid var(--line2)', color: '#3a4049', fontWeight: 600, fontSize: 12.5, fontFamily: "'Pretendard',sans-serif" }}>
<Icon name="refresh" size={14} stroke="#3a4049" /> 전적 갱신
</button>
</div>
</div>
<div style={{ display: 'flex', gap: 14, flexShrink: 0 }}>
<WinRing wr={p.wr} size={88} sw={8} />
<WinRing wr={SA.kdPct(p.kda)} size={88} sw={8} label="킬뎃" color={PSPT} />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(9,1fr)', borderTop: '1px solid var(--line)' }}>
{stats.map(([lab, val, col], i) => (
<div key={lab} style={{ padding: '12px 6px', textAlign: 'center', borderLeft: i ? '1px solid var(--line)' : 'none', background: i % 2 ? '#fff' : '#fafbfc' }}>
<div style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600, marginBottom: 5 }}>{lab}</div>
<div style={{ fontFamily: PSCH, fontWeight: 700, fontSize: 14.5, color: col }}>{val}</div>
</div>
))}
</div>
</div>
);
}
// ── 시즌별 랭크전 전적 (시즌 선택 · 솔로/파티) ──
function RankedSeasons({ p }) {
const seasons = p.rankedSeasons;
const [sel, setSel] = React.useState(seasons[0].season);
const isAll = sel === '통산';
const cur = seasons.find((s) => s.season === sel) || seasons[0];
const agg = (key) => {
let M = 0, W = 0, L = 0, kdW = 0, hsW = 0;
seasons.forEach((s) => { const d = s[key]; M += d.matches; W += d.w; L += d.l; kdW += d.kd * d.matches; hsW += d.hs * d.matches; });
return { matches: M, w: W, l: L, wr: M ? Math.round(W / M * 100) : 0, kd: M ? Math.round(kdW / M) : 0, hs: M ? Math.round(hsW / M) : 0, tier: null };
};
const rows = [['솔로', isAll ? agg('solo') : cur.solo], ['파티', isAll ? agg('party') : cur.party]];
const sel2 = (
<select value={sel} onChange={(e) => setSel(e.target.value)} style={{ fontFamily: "'Pretendard',sans-serif", fontSize: 12, fontWeight: 700, color: 'var(--ink)', border: '1px solid var(--line2)', borderRadius: 4, padding: '5px 8px', background: '#fff', cursor: 'pointer' }}>
<option value="통산">통산</option>
{seasons.map((s) => <option key={s.season} value={s.season}>{s.season}</option>)}
</select>
);
return (
<Panel title="시즌별 랭크전 전적" en="RANKED" right={sel2} bodyPad="0">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 660 }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--line2)', background: '#f7f8f9' }}>
<th style={psTh('left')}>구분</th><th style={{ ...psTh('left'), minWidth: 150 }}>티어</th><th style={psTh('right')}>티어 점수</th>
<th style={psTh('right')}>매치</th><th style={psTh('right')}>전적</th>
<th style={psTh('right')}>승률</th><th style={psTh('right')}>킬뎃</th>
</tr>
</thead>
<tbody>
{rows.map(([label, d], i) => (
<tr key={label} style={{ borderBottom: i === 0 ? '1px solid #eaecef' : 'none', background: i % 2 ? '#fff' : '#fbfbfc' }}>
<td style={{ ...psTd, fontWeight: 700, color: 'var(--ink)' }}>{label} 랭크전</td>
<td style={psTd}>{isAll ? <span style={{ color: 'var(--t3)' }}></span> : <TierChipS name={d.tier} div={d.div} />}</td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH, fontWeight: 600 }}>{isAll ? <span style={{ color: 'var(--t3)' }}></span> : <span>{window.SA.fmt(d.points)}<span style={{ fontSize: 10, color: 'var(--t3)', fontWeight: 500 }}> </span></span>}</td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH }}>{d.matches}</td>
<td style={{ ...psTd, textAlign: 'right', color: 'var(--t2)' }}>{recStr(d)}</td>
<td style={{ ...psTd, textAlign: 'right' }}><WrCell wr={d.wr} /></td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH, fontWeight: 600 }}>{d.kd}%</td>
</tr>
))}
</tbody>
</table>
</div>
</Panel>
);
}
// ── 클랜전 전적 (소속 클랜별) ──
function ClanRecords({ p }) {
const [open, setOpen] = React.useState(-1);
const [showAll, setShowAll] = React.useState(false);
const all = p.clanRecords;
const CAP = 4;
const list = showAll ? all : all.slice(0, CAP);
return (
<Panel title="클랜전 전적" en="CLAN" right={<span style={psSmall}>소속 클랜별 · 클릭 활동 기간</span>} bodyPad="0">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 600 }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--line2)', background: '#f7f8f9' }}>
<th style={psTh('left')}>클랜</th>
<th style={psTh('right')}>매치</th><th style={psTh('right')}>전적</th>
<th style={psTh('right')}>승률</th><th style={psTh('right')}>킬뎃</th>
</tr>
</thead>
<tbody>
{list.map((c, i) => {
const isOpen = open === i;
return (
<React.Fragment key={c.clan + i}>
<tr onClick={() => setOpen(isOpen ? -1 : i)} style={{ borderBottom: '1px solid #eaecef', background: isOpen ? '#f1f3f5' : (i % 2 ? '#fff' : '#fbfbfc'), cursor: 'pointer' }}>
<td style={psTd}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
<Icon name="chevR" size={13} stroke="var(--t3)" style={{ transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }} />
<span style={{ width: 26, height: 26, borderRadius: 4, background: '#23262b', display: 'grid', placeItems: 'center', fontFamily: PSCH, fontWeight: 700, fontSize: 9.5, color: '#fff' }}>{c.tag}</span>
<span style={{ fontWeight: 700, color: 'var(--ink)' }}>{c.clan}</span>
{i === 0 && <span style={{ fontSize: 9.5, fontWeight: 700, color: PSPT, border: `1px solid ${PSPT}`, borderRadius: 3, padding: '0 5px' }}>현재</span>}
<span style={{ fontSize: 10.5, color: 'var(--t3)' }}>가입 {c.periods.length}</span>
</span>
</td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH }}>{c.matches}</td>
<td style={{ ...psTd, textAlign: 'right', color: 'var(--t2)' }}>{recStr(c)}</td>
<td style={{ ...psTd, textAlign: 'right' }}><WrCell wr={c.wr} /></td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH, fontWeight: 600 }}>{c.kd}%</td>
</tr>
{isOpen && (
<tr>
<td colSpan={5} style={{ padding: '13px 16px 15px', background: '#f7f8f9', borderBottom: '1px solid #eaecef' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t3)', marginBottom: 9, letterSpacing: '.2px' }}>활동 기간 · {c.periods.length} 가입</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{c.periods.map((pd, j) => (
<div key={j} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', background: '#fff', border: '1px solid var(--line)', borderRadius: 5 }}>
<span style={{ fontFamily: PSCH, fontWeight: 700, fontSize: 11.5, color: '#fff', background: PSPT, borderRadius: 3, padding: '1px 7px', flexShrink: 0 }}>{c.periods.length - j}</span>
<Icon name="clock" size={14} stroke="var(--t3)" />
<span style={{ fontFamily: PSCH, fontSize: 13, color: 'var(--ink)', fontWeight: 700 }}>{pd.from} ~ {pd.to}</span>
<span style={{ fontSize: 12, color: 'var(--t3)' }}> {pd.months}개월</span>
</div>
))}
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
{all.length > CAP && (
<button onClick={() => setShowAll(!showAll)} style={{ width: '100%', padding: '10px 0', border: 'none', borderTop: '1px solid var(--line)', background: '#fff', cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12.5, color: 'var(--t2)' }}>
{showAll ? '접기 ▲' : `소속했던 클랜 더보기 +${all.length - CAP}`}
</button>
)}
</Panel>
);
}
// ── 최근 동향 추세 그래프 (승률 / 킬뎃 시계열) ──
function TrendChart({ p }) {
const [period, setPeriod] = React.useState('주별');
const data = period === '일별' ? p.trend.daily : p.trend.weekly;
const W = 720, H = 250, padL = 38, padR = 16, padT = 18, padB = 34;
const iw = W - padL - padR, ih = H - padT - padB;
const n = data.length;
const x = (i) => padL + (n === 1 ? iw / 2 : (i / (n - 1)) * iw);
const y = (v) => padT + (1 - (v - 20) / (90 - 20)) * ih;
const path = (key) => data.map((d, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)} ${y(d[key]).toFixed(1)}`).join(' ');
const area = (key) => `${path(key)} L${x(n - 1).toFixed(1)} ${(padT + ih).toFixed(1)} L${x(0).toFixed(1)} ${(padT + ih).toFixed(1)} Z`;
const WR = '#2f6fed', KD = PSPT;
const grid = [20, 40, 60, 80];
const last = data[n - 1];
const first = data[0];
const delta = (key) => last[key] - first[key];
const dchip = (label, key, col) => {
const dv = delta(key);
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
<span style={{ width: 11, height: 3, borderRadius: 2, background: col }} />
<span style={{ fontSize: 12, color: 'var(--t2)', fontWeight: 600 }}>{label}</span>
<span style={{ fontFamily: PSCH, fontWeight: 700, fontSize: 13, color: col }}>{last[key]}%</span>
<span style={{ fontFamily: PSCH, fontWeight: 700, fontSize: 11.5, color: dv >= 0 ? '#16a34a' : '#e5484d' }}>
{dv >= 0 ? '▲' : '▼'}{Math.abs(dv)}
</span>
</div>
);
};
const labelStep = Math.ceil(n / 8);
return (
<Panel title="최근 동향" en="TREND" right={<Seg tabs={['일별', '주별']} value={period} onChange={setPeriod} />} bodyPad="14px 16px 16px">
<div style={{ display: 'flex', gap: 20, marginBottom: 10, paddingLeft: 4 }}>
{dchip('승률', 'wr', WR)}
{dchip('킬뎃', 'kd', KD)}
<span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--t3)' }}>{period === '일별' ? '최근 14일' : '최근 10주'} 추이</span>
</div>
<svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
<defs>
<linearGradient id="wrGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={WR} stopOpacity="0.16" /><stop offset="100%" stopColor={WR} stopOpacity="0" />
</linearGradient>
</defs>
{grid.map((g) => (
<g key={g}>
<line x1={padL} y1={y(g)} x2={W - padR} y2={y(g)} stroke="#eceef1" strokeWidth="1" />
<text x={padL - 8} y={y(g) + 3.5} textAnchor="end" fontSize="10.5" fill="#949aa3" fontFamily={PSCH}>{g}</text>
</g>
))}
{data.map((d, i) => (i % labelStep === 0 || i === n - 1) && (
<text key={i} x={x(i)} y={H - 12} textAnchor="middle" fontSize="10.5" fill="#949aa3">{d.label}</text>
))}
<path d={area('wr')} fill="url(#wrGrad)" />
<path d={path('wr')} fill="none" stroke={WR} strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round" />
<path d={path('kd')} fill="none" stroke={KD} strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round" strokeDasharray="1 0" />
{data.map((d, i) => (
<g key={'p' + i}>
<circle cx={x(i)} cy={y(d.wr)} r={i === n - 1 ? 4 : 2.6} fill="#fff" stroke={WR} strokeWidth="2" />
<circle cx={x(i)} cy={y(d.kd)} r={i === n - 1 ? 4 : 2.6} fill="#fff" stroke={KD} strokeWidth="2" />
</g>
))}
</svg>
</Panel>
);
}
// ── 맵별 전적 ──
function MapRecords({ p }) {
const [showAll, setShowAll] = React.useState(false);
const all = p.mapRecords;
const CAP = 8;
const list = showAll ? all : all.slice(0, CAP);
return (
<Panel title="맵별 전적" en="BY MAP" right={<span style={psSmall}>매치 많은 · {all.length} </span>} bodyPad="0">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 560 }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--line2)', background: '#f7f8f9' }}>
<th style={{ ...psTh('left'), width: 30 }}>#</th>
<th style={psTh('left')}></th>
<th style={psTh('right')}>매치</th><th style={psTh('right')}>전적</th>
<th style={psTh('right')}>승률</th><th style={psTh('right')}>킬뎃</th>
</tr>
</thead>
<tbody>
{list.map((m, i) => (
<tr key={m.name + i} style={{ borderBottom: i < list.length - 1 ? '1px solid #eaecef' : 'none', background: i % 2 ? '#fff' : '#fbfbfc' }}>
<td style={{ ...psTd, textAlign: 'center', fontFamily: PSCH, fontWeight: 700, fontSize: 11.5, color: 'var(--t3)' }}>{i + 1}</td>
<td style={{ ...psTd, fontWeight: 700, color: 'var(--ink)' }}>{m.name}</td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH }}>{m.matches}</td>
<td style={{ ...psTd, textAlign: 'right', color: 'var(--t2)' }}>{recStr(m)}</td>
<td style={{ ...psTd, textAlign: 'right' }}><WrCell wr={m.wr} /></td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH, fontWeight: 600 }}>{m.kd}%</td>
</tr>
))}
</tbody>
</table>
</div>
{all.length > CAP && (
<button onClick={() => setShowAll(!showAll)} style={{ width: '100%', padding: '10px 0', border: 'none', borderTop: '1px solid var(--line)', background: '#fff', cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12.5, color: 'var(--t2)' }}>
{showAll ? '접기 ▲' : `맵 전체 보기 +${all.length - CAP}`}
</button>
)}
</Panel>
);
}
// ── 매치 기록 ──
function TeamTable({ result, color, players }) {
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '8px 12px', background: `${color}12` }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: color }} />
<span style={{ fontWeight: 800, fontSize: 12.5, color }}>{result}</span>
<span style={{ fontSize: 10.5, color: 'var(--t3)', fontWeight: 600 }}>5</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--line)' }}>
<th style={{ ...psTh('left'), padding: '7px 12px' }}>플레이어</th>
<th style={{ ...psTh('right'), padding: '7px 8px' }}>K / D / A</th>
<th style={{ ...psTh('right'), padding: '7px 8px' }}>킬뎃</th>
<th style={{ ...psTh('right'), padding: '7px 8px' }}>헤드샷</th>
<th style={{ ...psTh('right'), padding: '7px 12px' }}>데미지</th>
</tr>
</thead>
<tbody>
{players.map((pl, j) => {
const pct = Math.round(pl.k / (pl.k + pl.d) * 100);
return (
<tr key={j} style={{ borderBottom: j < players.length - 1 ? '1px solid #f1f2f4' : 'none', background: pl.isOwner ? `${color}0c` : '#fff' }}>
<td style={{ padding: '7px 12px', maxWidth: 150 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
<RankBadge rank={pl.rank} size={22} />
<span style={{ fontSize: 12.5, fontWeight: pl.isOwner ? 800 : 600, color: pl.isOwner ? color : 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{pl.isOwner && <span style={{ fontSize: 9.5, fontWeight: 800, marginRight: 4, color }}>ME</span>}{pl.name}
</span>
</span>
</td>
<td style={{ padding: '7px 8px', textAlign: 'right', fontFamily: PSCH, fontSize: 12.5 }}>
<span style={{ color: 'var(--ink)' }}>{pl.k}</span>
<span style={{ color: 'var(--t3)', margin: '0 2px' }}>/</span>
<span style={{ color: '#e5484d' }}>{pl.d}</span>
<span style={{ color: 'var(--t3)', margin: '0 2px' }}>/</span>
<span style={{ color: '#48515f' }}>{pl.a}</span>
</td>
<td style={{ padding: '7px 8px', textAlign: 'right', fontFamily: PSCH, fontSize: 12.5, fontWeight: 600, color: PSPT }}>{pct}%</td>
<td style={{ padding: '7px 8px', textAlign: 'right', fontFamily: PSCH, fontSize: 12.5, color: 'var(--t2)' }}>{pl.hs}%</td>
<td style={{ padding: '7px 12px', textAlign: 'right', fontFamily: PSCH, fontSize: 12.5, color: 'var(--t2)' }}>{window.SA.fmt(pl.dmg)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
function MatchRow({ m, i, last }) {
const [open, setOpen] = React.useState(false);
const pct = Math.round(m.k / (m.k + m.d) * 100);
const winTeam = m.winSide === 'blue' ? m.teams.blue : m.teams.red;
const loseTeam = m.winSide === 'blue' ? m.teams.red : m.teams.blue;
return (
<div style={{ borderBottom: last ? 'none' : '1px solid #f1f2f4' }}>
<div onClick={() => setOpen(!open)} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '11px 16px', cursor: 'pointer',
borderLeft: `3px solid ${m.win ? PSPT : '#e5484d'}`, background: open ? '#f7f8f9' : 'transparent' }}>
<div style={{ width: 40, flexShrink: 0 }}>
<span style={{ fontFamily: PSCH, fontWeight: 800, fontSize: 13, color: m.win ? PSPT : '#e5484d' }}>{m.win ? '승' : '패'}</span>
</div>
<div style={{ width: 132, minWidth: 0, flexShrink: 0 }}>
<div style={{ fontSize: 13, color: 'var(--ink)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.map}</div>
<div style={{ fontSize: 11, color: 'var(--t3)' }}>{m.mode}</div>
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
<KdaChip k={m.k} d={m.d} a={m.a} />
<span style={{ fontSize: 11.5, color: 'var(--t3)' }}>킬뎃 {pct}%</span>
</div>
<div style={{ textAlign: 'right', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 11, color: 'var(--t3)' }}>{m.ago}</span>
<Icon name="chevR" size={15} stroke="var(--t3)" style={{ transform: open ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }} />
</div>
</div>
{open && (
<div style={{ padding: '14px 16px 16px', background: '#f7f8f9', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<div style={{ border: '1px solid var(--line)', borderRadius: 6, overflow: 'hidden', background: '#fff' }}>
<TeamTable result="승리" color={PSPT} players={winTeam} />
</div>
<div style={{ border: '1px solid var(--line)', borderRadius: 6, overflow: 'hidden', background: '#fff' }}>
<TeamTable result="패배" color="#e5484d" players={loseTeam} />
</div>
</div>
)}
</div>
);
}
function MatchHistory({ p }) {
const TABS = ['전체', '클랜전', '솔로 랭크전', '파티 랭크전', '클랜 랭크전', '토너먼트'];
const [tab, setTab] = React.useState('전체');
React.useEffect(() => setTab('전체'), [p.name]);
const ms = tab === '전체' ? p.matches : p.matches.filter((m) => m.mode === tab);
const w = ms.filter((m) => m.win).length;
return (
<Panel title="매치 기록" en="MATCHES" right={<span style={psSmall}>{ms.length ? `최근 ${ms.length}전 · ${w}${ms.length - w}패 · 클릭 시 상세` : '클릭 시 상세'}</span>} bodyPad="0">
{/* 모드 서브탭 (가로 스크롤) */}
<div style={{ display: 'flex', gap: 6, padding: '11px 14px', borderBottom: '1px solid var(--line)', overflowX: 'auto', whiteSpace: 'nowrap' }}>
{TABS.map((t) => {
const on = tab === t;
return (
<button key={t} onClick={() => setTab(t)} style={{ flexShrink: 0, padding: '6px 13px', borderRadius: 20, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12,
border: `1px solid ${on ? PSPT : 'var(--line2)'}`, background: on ? PSPT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{t}</button>
);
})}
</div>
{ms.map((m, i) => (
<MatchRow key={i} m={m} i={i} last={i === ms.length - 1} />
))}
{ms.length === 0 && (
<div style={{ padding: '26px 0', textAlign: 'center', fontSize: 13, color: 'var(--t3)' }}>해당 모드의 최근 매치가 없어요.</div>
)}
</Panel>
);
}
// ── 최근 전적 및 동향 ──
function RecentTrend({ p }) {
const recent = p.matches.slice(0, 12);
const wins = recent.filter((m) => m.win).length;
const losses = recent.length - wins;
const wr = Math.round(wins / recent.length * 100);
const avgKd = Math.round(recent.reduce((a, m) => a + m.k / (m.k + m.d) * 100, 0) / recent.length);
return (
<Panel title="최근 전적 및 동향" en="RECENT" bodyPad="14px 16px 16px">
<div style={{ display: 'flex', gap: 18, marginBottom: 14, justifyContent: 'center' }}>
<WinRing wr={wr} size={72} sw={7} label="최근 승률" />
<WinRing wr={avgKd} size={72} sw={7} label="평균 킬뎃" color={PSPT} />
</div>
<div style={{ textAlign: 'center', fontSize: 13, color: 'var(--t2)', marginBottom: 12 }}>
최근 {recent.length} <b style={{ color: PSPT }}>{wins}</b> <b style={{ color: '#e5484d' }}>{losses}</b>
</div>
<div style={{ display: 'flex', gap: 4 }}>
{recent.slice().reverse().map((m, i) => (
<span key={i} style={{ flex: 1, height: 20, borderRadius: 3, display: 'grid', placeItems: 'center', fontSize: 9.5, fontWeight: 800, fontFamily: PSCH,
background: m.win ? 'rgba(72,81,95,.9)' : 'rgba(229,72,77,.9)', color: '#fff' }}>{m.win ? '승' : '패'}</span>
))}
</div>
<div style={{ fontSize: 10.5, color: 'var(--t3)', marginTop: 7 }}> 최신 경기 흐름</div>
</Panel>
);
}
// ── 자주 함께한 유저 ──
function Companions({ p, onPick }) {
const SA = window.SA;
return (
<Panel title="자주 함께한 유저" en="WITH" bodyPad="6px 8px 8px">
{SA.friends.slice(0, 4).map((f) => (
<button key={f.name} onClick={() => onPick(f)} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', border: 'none', background: 'transparent', padding: '8px 8px', borderRadius: 5, cursor: 'pointer', textAlign: 'left', fontFamily: "'Pretendard',sans-serif" }}
onMouseEnter={(e) => e.currentTarget.style.background = '#f5f6f8'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
<RankBadge rank={f.rank} size={30} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ fontSize: 13, color: 'var(--ink)', fontWeight: 600, flexShrink: 0, maxWidth: '58%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{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: 11, color: 'var(--t3)', marginTop: 1 }}>
함께 {f.together.games}<span style={{ color: 'var(--line2)', margin: '0 5px' }}>·</span><b style={{ fontFamily: PSCH, color: PSPT }}>{f.together.w}</b> <b style={{ fontFamily: PSCH, color: '#e5484d' }}>{f.together.l}</b>
<span style={{ color: 'var(--line2)', margin: '0 5px' }}>·</span>
<span style={{ fontFamily: PSCH, fontWeight: 600, color: wrColor(Math.round(f.together.w / f.together.games * 100)) }}>승률 {Math.round(f.together.w / f.together.games * 100)}%</span>
</div>
</div>
<Icon name="chevR" size={15} stroke="var(--t3)" />
</button>
))}
</Panel>
);
}
// ── 모드별 전적 ──
function ModeRecords({ p }) {
return (
<Panel title="모드별 전적" en="BY MODE" bodyPad="0">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 560 }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--line2)', background: '#f7f8f9' }}>
<th style={psTh('left')}>모드</th>
<th style={psTh('right')}>매치</th><th style={psTh('right')}>전적</th>
<th style={psTh('right')}>승률</th><th style={psTh('right')}>킬뎃</th>
</tr>
</thead>
<tbody>
{p.modeRecords.map((m, i) => (
<tr key={m.name + i} style={{ borderBottom: i < p.modeRecords.length - 1 ? '1px solid #eaecef' : 'none', background: i % 2 ? '#fff' : '#fbfbfc' }}>
<td style={{ ...psTd, fontWeight: 700, color: 'var(--ink)' }}>{m.name}</td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH }}>{m.matches}</td>
<td style={{ ...psTd, textAlign: 'right', color: 'var(--t2)' }}>{recStr(m)}</td>
<td style={{ ...psTd, textAlign: 'right' }}><WrCell wr={m.wr} /></td>
<td style={{ ...psTd, textAlign: 'right', fontFamily: PSCH, fontWeight: 600 }}>{m.kd}%</td>
</tr>
))}
</tbody>
</table>
</div>
</Panel>
);
}
Object.assign(window, { ProfileSummary, RankedSeasons, ClanRecords, MapRecords, ModeRecords, MatchHistory, RecentTrend, Companions, TrendChart });