527 lines
31 KiB
React
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 });
|