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

371 lines
24 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.Panel, Icon, RankBadge, Emblem, SA
// ─────────────────────────────────────────────────────────────
const CLPT = '#48515f';
const CLCH = "'Chakra Petch',sans-serif";
function RoleBadge({ role, theme }) {
const map = {
'클랜장': { bg: theme, color: '#fff' },
'운영진': { bg: `${theme}22`, color: theme },
'정예': { bg: '#eef0f2', color: '#5b626c' },
'일반': { bg: 'transparent', color: '#949aa3' },
};
const s = map[role] || map['일반'];
return <span style={{ fontSize: 10, fontWeight: 700, padding: '1px 7px', borderRadius: 4, background: s.bg, color: s.color, flexShrink: 0 }}>{role}</span>;
}
// ── 멤버 ──
function ClanMembers({ clan, onPick }) {
const groups = ['클랜장', '운영진', '정예', '일반'];
const byRole = (r) => clan.members.filter((m) => m.role === r);
return (
<Panel title="클랜원" en="MEMBERS" right={<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>{clan.totalMembers} / {clan.memberMax}</span>} bodyPad="6px 0 6px">
{groups.map((g) => {
const list = byRole(g);
if (!list.length) return null;
return (
<div key={g}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '8px 16px 4px' }}>
<span style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--t3)', letterSpacing: '.3px' }}>{g} · {list.length}</span>
<span style={{ fontSize: 9.5, fontWeight: 600, color: 'var(--t3)' }}>기여도</span>
</div>
{list.map((m, i) => (
<button key={m.name + i} onClick={() => onPick && onPick(m.name)} style={{ display: 'flex', alignItems: 'center', gap: 9, width: '100%', border: 'none', background: 'transparent', padding: '8px 16px', cursor: 'pointer', textAlign: 'left', fontFamily: "'Pretendard',sans-serif" }}
onMouseEnter={(e) => e.currentTarget.style.background = '#f6f7f8'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
{m.rank && <RankBadge rank={m.rank} size={26} />}
<span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.name}</span>
<span style={{ display: 'inline-flex', alignItems: 'baseline', gap: 3, flexShrink: 0 }}>
<span style={{ fontFamily: CLCH, fontSize: 12.5, color: CLPT, fontWeight: 700 }}>{window.SA.fmt(m.contrib)}</span>
<span style={{ fontSize: 9.5, color: 'var(--t3)', fontWeight: 600 }}>P</span>
</span>
</button>
))}
</div>
);
})}
</Panel>
);
}
// ── 정기전 일정 ──
function ClanSchedule({ clan }) {
const t = CLPT;
const [items, setItems] = React.useState(clan.schedule);
const [open, setOpen] = React.useState(false);
const [day, setDay] = React.useState('월');
const [time, setTime] = React.useState('21:00');
const [title, setTitle] = React.useState('');
React.useEffect(() => { setItems(clan.schedule); setOpen(false); }, [clan.name]);
const register = () => {
const name = title.trim();
if (!name) return;
setItems(items.map((s) => s.day === day ? { ...s, on: true, time, title: name } : s));
setTitle('');
setOpen(false);
};
const remove = (d) => setItems(items.map((s) => s.day === d ? { ...s, on: false, time: '—', title: '휴식' } : s));
const inputStyle = { border: '1px solid var(--line2)', borderRadius: 6, padding: '9px 11px', fontSize: 13, fontFamily: "'Pretendard',sans-serif", fontWeight: 600, color: 'var(--ink)', outline: 'none', boxSizing: 'border-box' };
return (
<Panel title="클랜 일정" en="SCHEDULE" right={clan.isMine ? (
<button onClick={() => setOpen(true)} style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, color: t, padding: 0 }}>+ 일정 등록</button>
) : null} bodyPad="12px 14px 14px">
<div style={{ display: 'flex', gap: 5 }}>
{items.map((s) => (
<div key={s.day} style={{ flex: 1, textAlign: 'center', padding: '9px 2px', borderRadius: 6, background: s.on ? t : '#f4f5f7', color: s.on ? '#fff' : 'var(--t3)' }}>
<div style={{ fontSize: 12, fontWeight: 800 }}>{s.day}</div>
<div style={{ fontFamily: CLCH, fontSize: 9.5, marginTop: 2, opacity: .9 }}>{s.time}</div>
</div>
))}
</div>
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
{items.filter((s) => s.on).map((s) => (
<div key={s.day} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
<span style={{ fontWeight: 800, color: t, width: 16 }}>{s.day}</span>
<span style={{ fontFamily: CLCH, color: 'var(--t2)', width: 44 }}>{s.time}</span>
<span style={{ color: 'var(--ink)', fontWeight: 600, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.title}</span>
{clan.isMine && (
<button onClick={() => remove(s.day)} title="일정 삭제" style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--t3)', fontSize: 14, lineHeight: 1, padding: '0 2px', flexShrink: 0 }}>×</button>
)}
</div>
))}
{items.every((s) => !s.on) && <div style={{ fontSize: 12, color: 'var(--t3)', textAlign: 'center', padding: '6px 0' }}>등록된 일정이 없어요.</div>}
</div>
{/* 일정 등록 모달 */}
{open && (
<div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 260, background: 'rgba(20,22,26,.55)', backdropFilter: 'blur(2px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, animation: 'sgFade .18s ease' }}>
<div onClick={(e) => e.stopPropagation()} style={{ width: 380, maxWidth: '100%', background: '#fff', borderRadius: 12, overflow: 'hidden', boxShadow: '0 24px 70px rgba(0,0,0,.3)', fontFamily: "'Pretendard',sans-serif" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 18px', borderBottom: '1px solid var(--line)' }}>
<span style={{ fontSize: 15, fontWeight: 800, color: 'var(--ink)' }}>일정 등록</span>
<button onClick={() => setOpen(false)} style={{ width: 28, height: 28, borderRadius: 6, border: 'none', background: '#eef0f2', color: '#5b626c', cursor: 'pointer', fontSize: 15 }}>×</button>
</div>
<div style={{ padding: '16px 18px' }}>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--t2)', marginBottom: 7 }}>요일</div>
<div style={{ display: 'flex', gap: 5, marginBottom: 14 }}>
{items.map((s) => (
<button key={s.day} onClick={() => setDay(s.day)} style={{ flex: 1, padding: '8px 0', borderRadius: 6, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 800, fontSize: 12.5,
border: `1px solid ${day === s.day ? t : 'var(--line2)'}`, background: day === s.day ? t : '#fff', color: day === s.day ? '#fff' : (s.on ? t : 'var(--t2)') }}>{s.day}</button>
))}
</div>
{items.find((s) => s.day === day && s.on) && (
<div style={{ fontSize: 11.5, color: 'var(--t3)', marginTop: -8, marginBottom: 12 }}>이미 일정이 있는 요일이에요. 등록하면 덮어써요.</div>
)}
<div style={{ display: 'flex', gap: 10, marginBottom: 14 }}>
<div style={{ width: 120 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--t2)', marginBottom: 7 }}>시간</div>
<input type="time" value={time} onChange={(e) => setTime(e.target.value)} style={{ ...inputStyle, width: '100%' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--t2)', marginBottom: 7 }}>일정명</div>
<input value={title} onChange={(e) => setTitle(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') register(); }} placeholder="예: 정기전, 내전 데이" style={{ ...inputStyle, width: '100%' }} />
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => setOpen(false)} style={{ flex: 1, padding: '11px 0', borderRadius: 7, border: '1px solid var(--line2)', background: '#fff', color: '#3a4049', fontWeight: 700, fontSize: 13.5, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>취소</button>
<button onClick={register} disabled={!title.trim()} style={{ flex: 1.4, padding: '11px 0', borderRadius: 7, border: 'none', background: title.trim() ? t : '#c4c8ce', color: '#fff', fontWeight: 700, fontSize: 13.5, cursor: title.trim() ? 'pointer' : 'not-allowed', fontFamily: "'Pretendard',sans-serif" }}>등록하기</button>
</div>
</div>
</div>
</div>
)}
</Panel>
);
}
// ── 주간 활동 (접속률) ──
function ClanActivity({ clan }) {
const days = ['월', '화', '수', '목', '금', '토', '일'];
const max = Math.max(...clan.attendance);
return (
<Panel title="주간 활동" en="ACTIVITY" right={<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>접속률</span>} bodyPad="14px 14px 12px">
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 7, height: 86 }}>
{clan.attendance.map((v, i) => (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 5 }}>
<span style={{ fontFamily: CLCH, fontSize: 10, color: 'var(--t3)', fontWeight: 600 }}>{v}%</span>
<div style={{ width: '100%', height: `${(v / max) * 58}px`, background: CLPT, borderRadius: '3px 3px 0 0', opacity: 0.55 + 0.45 * (v / max) }} />
<span style={{ fontSize: 10.5, color: 'var(--t3)' }}>{days[i]}</span>
</div>
))}
</div>
</Panel>
);
}
// ── 주간 매치 (모드별) ──
function ClanWeeklyMatches({ clan }) {
const days = ['월', '화', '수', '목', '금', '토', '일'];
const acts = clan.activity;
const cats = [
['clanWar', '클랜전', CLPT],
['clanRanked', '클랜 랭크전', '#6b7585'],
['soloRanked', '솔로 랭크전', '#99a1ae'],
['partyRanked', '파티 랭크전', '#cdd2d9'],
];
const sum = (a) => cats.reduce((s, [k]) => s + a[k], 0);
const max = Math.max(...acts.map(sum));
const totals = cats.map(([k, label, color]) => [label, acts.reduce((s, a) => s + a[k], 0), color]);
return (
<Panel title="주간 매치" en="MATCHES" right={<span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>모드별 판수</span>} bodyPad="14px 14px 12px">
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 7, height: 96 }}>
{acts.map((a, i) => (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 5 }}>
<span style={{ fontFamily: CLCH, fontSize: 10, color: 'var(--t3)', fontWeight: 600 }}>{sum(a)}</span>
<div style={{ width: '100%', height: `${(sum(a) / max) * 62}px`, display: 'flex', flexDirection: 'column-reverse', borderRadius: '3px 3px 0 0', overflow: 'hidden' }}>
{cats.map(([k, label, color]) => (
<div key={k} title={`${label} ${a[k]}`} style={{ height: `${(a[k] / sum(a)) * 100}%`, background: color }} />
))}
</div>
<span style={{ fontSize: 10.5, color: 'var(--t3)' }}>{days[i]}</span>
</div>
))}
</div>
<div style={{ marginTop: 11, paddingTop: 10, borderTop: '1px solid var(--line)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '7px 10px' }}>
{totals.map(([label, n, color]) => (
<span key={label} style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 11.5, color: 'var(--t2)', fontWeight: 600 }}>
<span style={{ width: 8, height: 8, borderRadius: 2, background: color, flexShrink: 0 }} />
{label} <b style={{ fontFamily: CLCH, color: 'var(--ink)', marginLeft: 'auto' }}>{n}</b>
</span>
))}
</div>
</Panel>
);
}
// ── 미니 페이저 ──
function ClanPager({ page, total, onPage }) {
if (total <= 1) return null;
const nav = (dis) => ({ width: 26, height: 26, borderRadius: 5, border: '1px solid var(--line2)', background: '#fff', cursor: dis ? 'default' : 'pointer', opacity: dis ? .4 : 1, display: 'grid', placeItems: 'center' });
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, padding: '11px 0 5px' }}>
<button onClick={() => onPage(page - 1)} disabled={page === 1} style={nav(page === 1)}><Icon name="chevL" size={13} stroke="var(--t2)" /></button>
{Array.from({ length: total }, (_, i) => i + 1).map((n) => (
<button key={n} onClick={() => onPage(n)} style={{ minWidth: 26, height: 26, padding: '0 5px', borderRadius: 5, cursor: 'pointer', fontFamily: CLCH, fontWeight: 700, fontSize: 11.5, border: n === page ? 'none' : '1px solid var(--line2)', background: n === page ? CLPT : '#fff', color: n === page ? '#fff' : 'var(--t2)' }}>{n}</button>
))}
<button onClick={() => onPage(page + 1)} disabled={page === total} style={nav(page === total)}><Icon name="chevR" size={13} stroke="var(--t2)" /></button>
</div>
);
}
// ── 공지 ──
function ClanNotices({ clan }) {
const PER = 4;
const [page, setPage] = React.useState(1);
React.useEffect(() => setPage(1), [clan.name]);
const total = Math.max(1, Math.ceil(clan.notices.length / PER));
const cur = Math.min(page, total);
const rows = clan.notices.slice((cur - 1) * PER, cur * PER);
return (
<Panel title="클랜 공지" en="NOTICE" bodyPad="2px 14px 6px">
{rows.map((n, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 2px', borderBottom: i < rows.length - 1 ? '1px solid #eaecef' : 'none', cursor: 'pointer' }}>
{n.pin && <span style={{ fontSize: 10, fontWeight: 800, color: CLPT, flexShrink: 0 }}>고정</span>}
<span style={{ flex: 1, fontSize: 13.5, color: '#33373d', fontWeight: n.pin ? 700 : 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.title}</span>
<span style={{ fontSize: 11, color: 'var(--t3)', flexShrink: 0 }}>{n.author} · {n.time}</span>
</div>
))}
<ClanPager page={cur} total={total} onPage={(n) => setPage(Math.max(1, Math.min(total, n)))} />
</Panel>
);
}
// ── 클랜 피드 ──
function ClanFeed({ clan }) {
const PER = 3;
const [items, setItems] = React.useState(clan.feed);
const [draft, setDraft] = React.useState('');
const [page, setPage] = React.useState(1);
React.useEffect(() => { setItems(clan.feed); setDraft(''); setPage(1); }, [clan.name]);
const total = Math.max(1, Math.ceil(items.length / PER));
const cur = Math.min(page, total);
const rows = items.slice((cur - 1) * PER, cur * PER);
const post = () => {
const text = draft.trim();
if (!text) return;
setItems([{ author: window.SA.me.name, role: '일반', time: '방금 전', text, likes: 0 }, ...items]);
setDraft('');
setPage(1);
};
return (
<Panel title="클랜 피드" en="FEED" bodyPad="6px 14px 12px">
{clan.isMine && (
<div style={{ display: 'flex', gap: 8, padding: '10px 0 12px', borderBottom: '1px solid var(--line)' }}>
<div style={{ width: 34, height: 34, borderRadius: '50%', background: `${CLPT}1c`, color: CLPT, display: 'grid', placeItems: 'center', fontWeight: 800, fontSize: 14, flexShrink: 0 }}>{window.SA.me.name.slice(0, 1)}</div>
<input value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') post(); }}
placeholder="클랜원들에게 소식을 남겨보세요"
style={{ flex: 1, minWidth: 0, border: '1px solid var(--line2)', borderRadius: 6, padding: '9px 12px', fontSize: 13, fontFamily: "'Pretendard',sans-serif", outline: 'none' }} />
<button onClick={post} disabled={!draft.trim()} style={{ padding: '0 16px', borderRadius: 6, border: 'none', cursor: draft.trim() ? 'pointer' : 'not-allowed', background: draft.trim() ? CLPT : '#c4c8ce', color: '#fff', fontWeight: 700, fontSize: 12.5, fontFamily: "'Pretendard',sans-serif", flexShrink: 0 }}>작성</button>
</div>
)}
{rows.map((f, i) => (
<div key={(cur - 1) * PER + i} style={{ display: 'flex', gap: 11, padding: '11px 0', borderBottom: i < rows.length - 1 ? '1px solid #f1f2f4' : 'none' }}>
<div style={{ width: 34, height: 34, borderRadius: '50%', background: `${CLPT}1c`, color: CLPT, display: 'grid', placeItems: 'center', fontWeight: 800, fontSize: 14, flexShrink: 0 }}>{f.author.slice(0, 1)}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)' }}>{f.author}</span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--t3)' }}>{f.time}</span>
</div>
<div style={{ fontSize: 13.5, color: '#3a4049', lineHeight: 1.5 }}>{f.text}</div>
<div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: 5, color: 'var(--t3)' }}>
<Icon name="heart" size={13} stroke="var(--t3)" /><span style={{ fontSize: 11.5, fontWeight: 600 }}>{f.likes}</span>
</div>
</div>
</div>
))}
<ClanPager page={cur} total={total} onPage={(n) => setPage(Math.max(1, Math.min(total, n)))} />
</Panel>
);
}
// ── 방명록 ──
function ClanGuestbook({ clan }) {
const PER = 4;
const [items, setItems] = React.useState(clan.guestbook);
const [draft, setDraft] = React.useState('');
const [page, setPage] = React.useState(1);
React.useEffect(() => { setItems(clan.guestbook); setDraft(''); setPage(1); }, [clan.name]);
const total = Math.max(1, Math.ceil(items.length / PER));
const cur = Math.min(page, total);
const rows = items.slice((cur - 1) * PER, cur * PER);
const post = () => {
const text = draft.trim();
if (!text) return;
setItems([{ author: window.SA.me.name, text, time: '방금 전' }, ...items]);
setDraft('');
setPage(1);
};
const [replyAt, setReplyAt] = React.useState(-1);
const [replyDraft, setReplyDraft] = React.useState('');
React.useEffect(() => { setReplyAt(-1); setReplyDraft(''); }, [clan.name]);
const postReply = (gi) => {
const text = replyDraft.trim();
if (!text) return;
setItems(items.map((g, idx) => idx === gi ? { ...g, replies: [...(g.replies || []), { author: window.SA.me.name, text, time: '방금 전' }] } : g));
setReplyDraft('');
setReplyAt(-1);
};
return (
<Panel title="방명록" en="GUESTBOOK" right={clan.isMine ? <span style={{ fontSize: 11, color: 'var(--t3)', fontWeight: 600 }}>클랜원은 답글만 있어요</span> : null} bodyPad="10px 14px 14px">
{!clan.isMine && (
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<input value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') post(); }} placeholder="따뜻한 한마디를 남겨보세요" style={{ flex: 1, minWidth: 0, border: '1px solid var(--line2)', borderRadius: 6, padding: '9px 12px', fontSize: 13, fontFamily: "'Pretendard',sans-serif", outline: 'none' }} />
<button onClick={post} disabled={!draft.trim()} style={{ padding: '0 16px', borderRadius: 6, border: 'none', background: draft.trim() ? CLPT : '#c4c8ce', color: '#fff', fontWeight: 700, fontSize: 13, cursor: draft.trim() ? 'pointer' : 'not-allowed', fontFamily: "'Pretendard',sans-serif", flexShrink: 0 }}>등록</button>
</div>
)}
{rows.map((g, i) => {
const gi = (cur - 1) * PER + i;
const replies = g.replies || [];
return (
<div key={gi} style={{ display: 'flex', gap: 9, padding: '9px 0', borderTop: '1px solid #f1f2f4' }}>
<div style={{ width: 28, height: 28, borderRadius: '50%', background: '#eef0f2', color: '#7a8089', display: 'grid', placeItems: 'center', fontWeight: 700, fontSize: 12, flexShrink: 0 }}>{g.author.slice(0, 1)}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 7 }}>
<span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink)' }}>{g.author}</span>
<span style={{ fontSize: 11, color: 'var(--t3)' }}>{g.time}</span>
{clan.isMine && (
<button onClick={() => { setReplyAt(replyAt === gi ? -1 : gi); setReplyDraft(''); }} style={{ marginLeft: 'auto', border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 11, color: CLPT, padding: 0, flexShrink: 0 }}>
{replyAt === gi ? '취소' : '답글'}
</button>
)}
</div>
<div style={{ fontSize: 13, color: '#3a4049', marginTop: 2 }}>{g.text}</div>
{/* 답글 목록 */}
{replies.map((rp, j) => (
<div key={j} style={{ display: 'flex', gap: 9, marginTop: 13, paddingLeft: 10, borderLeft: `2px solid ${CLPT}33` }}>
<div style={{ width: 28, height: 28, borderRadius: '50%', background: `${CLPT}1c`, color: CLPT, display: 'grid', placeItems: 'center', fontWeight: 800, fontSize: 12, flexShrink: 0 }}>{rp.author.slice(0, 1)}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 7 }}>
<span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink)' }}>{rp.author}</span>
<span style={{ fontSize: 9.5, fontWeight: 700, color: CLPT, border: `1px solid ${CLPT}55`, borderRadius: 3, padding: '0 4px' }}>클랜원</span>
<span style={{ fontSize: 11, color: 'var(--t3)' }}>{rp.time}</span>
</div>
<div style={{ fontSize: 13, color: '#3a4049', marginTop: 4 }}>{rp.text}</div>
</div>
</div>
))}
{/* 답글 입력 */}
{replyAt === gi && (
<div style={{ display: 'flex', gap: 7, marginTop: 13, paddingLeft: 10, borderLeft: `2px solid ${CLPT}33` }}>
<input autoFocus value={replyDraft} onChange={(e) => setReplyDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') postReply(gi); }} placeholder="답글을 남겨보세요"
style={{ flex: 1, minWidth: 0, border: '1px solid var(--line2)', borderRadius: 6, padding: '7px 10px', fontSize: 12.5, fontFamily: "'Pretendard',sans-serif", outline: 'none' }} />
<button onClick={() => postReply(gi)} disabled={!replyDraft.trim()} style={{ padding: '0 13px', borderRadius: 6, border: 'none', background: replyDraft.trim() ? CLPT : '#c4c8ce', color: '#fff', fontWeight: 700, fontSize: 11.5, cursor: replyDraft.trim() ? 'pointer' : 'not-allowed', fontFamily: "'Pretendard',sans-serif", flexShrink: 0 }}>등록</button>
</div>
)}
</div>
</div>
);
})}
<ClanPager page={cur} total={total} onPage={(n) => setPage(Math.max(1, Math.min(total, n)))} />
</Panel>
);
}
Object.assign(window, { ClanMembers, ClanSchedule, ClanActivity, ClanWeeklyMatches, ClanNotices, ClanFeed, ClanGuestbook });