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

458 lines
36 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.ClanPlaza · 의존: Nav, Panel, Icon, Emblem, ClanRecruits, ClanDB
// ─────────────────────────────────────────────────────────────
const PZPT = '#48515f';
const PZCH = "'Chakra Petch',sans-serif";
function pzAgeLabel(cd) {
if (cd.ageMin && cd.ageMax) return `${cd.ageMin}~${cd.ageMax}`;
if (cd.ageMin) return `${cd.ageMin}세 이상`;
if (cd.ageMax) return `${cd.ageMax}세 이하`;
return '전연령';
}
function ContactGlyph({ type }) {
if (type === '디스코드') {
return (
<svg width="19" height="19" viewBox="0 0 24 24" fill="#fff" aria-hidden="true">
<path d="M20.3 4.4A19.8 19.8 0 0 0 15.4 3l-.25.5a14.6 14.6 0 0 1 4.3 1.4c-2-1-4.2-1.5-6.45-1.5s-4.45.5-6.45 1.5A14.6 14.6 0 0 1 10.85 3.5L10.6 3a19.8 19.8 0 0 0-4.9 1.4C2.6 9 1.75 13.4 2.15 17.75A19.9 19.9 0 0 0 8.2 20.8l.5-.85a13 13 0 0 1-2-1l.5-.36a14.2 14.2 0 0 0 12.1 0l.5.36c-.62.4-1.3.74-2 1l.5.85a19.9 19.9 0 0 0 6.05-3.05c.5-5-0.86-9.36-3.85-13.35ZM9 15.3c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.82.9 1.8 2c0 1.1-.8 2-1.8 2Zm6 0c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.82.9 1.8 2c0 1.1-.8 2-1.8 2Z"/>
</svg>
);
}
return (
<svg width="19" height="19" viewBox="0 0 24 24" fill="#191600" aria-hidden="true">
<path d="M12 4C7 4 3 7.1 3 11c0 2.5 1.7 4.7 4.2 5.9-.18.65-.66 2.35-.76 2.72 0 0-.02.13.07.18.08.05.2 0 .2 0 .26-.04 2.96-1.96 3.5-2.34.58.08 1.18.13 1.79.13 5 0 9-3.1 9-7C21 7.1 17 4 12 4Z"/>
</svg>
);
}
function RecruitCard({ rc, onOpen, onDetail }) {
const fill = Math.round((rc.cur / rc.max) * 100);
const isMine = rc.name === (window.SA.me.clan || '');
return (
<div onClick={() => onDetail(rc)} style={{ background: '#fff', border: `1px solid ${isMine ? PZPT : 'var(--line)'}`, borderRadius: 8, overflow: 'hidden', display: 'flex', cursor: 'pointer', boxShadow: '0 1px 2px rgba(0,0,0,.04)', transition: 'box-shadow .15s' }}
onMouseEnter={(e) => e.currentTarget.style.boxShadow = '0 4px 14px rgba(0,0,0,.1)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = '0 1px 2px rgba(0,0,0,.04)'}>
{/* 홍보 포스터 (3:4, 유저 업로드) */}
<div style={{ width: 150, flexShrink: 0, alignSelf: 'stretch', borderRight: '1px solid var(--line)', background: '#f4f5f7' }}>
<image-slot id={`recruit-poster-${rc.name}`} shape="rect" placeholder="홍보 포스터" style={{ display: 'block', width: '100%', height: '100%' }}></image-slot>
</div>
<div style={{ flex: 1, minWidth: 0, padding: '13px 15px', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
<div style={{ flexShrink: 0 }}><window.Emblem cfg={rc.emblem} size={30} /></div>
<span style={{ fontSize: 15.5, fontWeight: 800, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{rc.name}</span>
{isMine && <span style={{ flexShrink: 0, fontSize: 9.5, fontWeight: 700, color: PZPT, border: `1px solid ${PZPT}`, borderRadius: 3, padding: '1px 6px' }}> 모집글</span>}
</div>
<div style={{ marginTop: 7, display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
{rc.recruitTypes.map((t) => (
<span key={t} style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--ink)', border: '1px solid var(--line2)', background: '#fff', padding: '2px 8px', borderRadius: 4 }}>{t}</span>
))}
{(() => {
const cd = rc.conditions;
const age = pzAgeLabel(cd);
return [age !== '전연령' ? age : null, cd.voice ? '보이스 채팅 필수' : null].filter(Boolean).map((b) => (
<span key={b} style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--t2)', background: '#eef0f2', padding: '2px 8px', borderRadius: 4 }}>{b}</span>
));
})()}
</div>
<p style={{ margin: '8px 0 0', fontSize: 12, lineHeight: 1.5, color: '#4a505a', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{rc.intro}</p>
<div style={{ marginTop: 'auto', paddingTop: 10, display: 'flex', alignItems: 'center', gap: 9, fontSize: 11.5, color: 'var(--t2)', flexWrap: 'wrap' }}>
<span><b style={{ color: PZPT, fontFamily: PZCH, fontWeight: 700 }}>{rc.rank}</b></span>
<span style={{ color: 'var(--line2)' }}>·</span>
<span>승률 <b style={{ color: 'var(--ink)', fontFamily: PZCH }}>{rc.weeklyWr}%</b></span>
<span style={{ color: 'var(--line2)' }}>·</span>
<span style={{ fontFamily: PZCH }}>{rc.record.matches} <b style={{ color: PZPT }}>{rc.record.w}</b> <b style={{ color: '#e5484d' }}>{rc.record.l}</b></span>
</div>
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', fontSize: 11, marginBottom: 4 }}>
<span style={{ color: 'var(--t3)', fontWeight: 600 }}>클랜원</span>
<span style={{ fontFamily: PZCH, fontWeight: 700, color: 'var(--ink)' }}>{rc.cur}<span style={{ color: 'var(--t3)' }}>/{rc.max}</span> <span style={{ color: rc.openings <= 3 ? '#e5484d' : PZPT, fontWeight: 700 }}>· {rc.openings}자리</span></span>
</div>
<div style={{ height: 6, background: '#d4d8de', borderRadius: 4, overflow: 'hidden', boxShadow: 'inset 0 0 0 1px rgba(0,0,0,.05)' }}>
<div style={{ width: `${fill}%`, height: '100%', background: rc.openings <= 3 ? '#e5484d' : PZPT, borderRadius: 4 }} />
</div>
</div>
<button onClick={(e) => { e.stopPropagation(); }} style={{ marginTop: 11, padding: '8px 0', borderRadius: 6, border: 'none', background: PZPT, color: '#fff', fontWeight: 700, fontSize: 12.5, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>가입 신청</button>
</div>
</div>
);
}
// ── 상세 모집글 ──
function RecruitDetail({ rc, onClose, onOpenClan }) {
return (
<div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 240, background: 'rgba(20,22,26,.62)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 20px', overflowY: 'auto', animation: 'sgFade .2s ease' }}>
<div onClick={(e) => e.stopPropagation()} style={{ width: 860, maxWidth: '100%', margin: 'auto', background: '#fff', borderRadius: 14, overflow: 'hidden', boxShadow: '0 24px 70px rgba(0,0,0,.34)', fontFamily: "'Pretendard',sans-serif" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 20px', borderBottom: '1px solid var(--line)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
<window.Emblem cfg={rc.emblem} size={34} />
<span style={{ fontSize: 17, fontWeight: 800, color: 'var(--ink)' }}>{rc.name}</span>
<span style={{ fontSize: 11.5, color: 'var(--t3)', fontWeight: 600 }}>모집글 · {rc.createdAgo} </span>
</div>
<button onClick={onClose} style={{ width: 30, height: 30, borderRadius: 6, border: 'none', background: '#eef0f2', color: '#5b626c', cursor: 'pointer', fontSize: 16 }}>×</button>
</div>
<div style={{ display: 'flex', gap: 0, alignItems: 'stretch' }}>
{/* 3:4 포스터 */}
<div style={{ width: 360, flexShrink: 0, background: '#f4f5f7', borderRight: '1px solid var(--line)' }}>
<image-slot id={`recruit-poster-${rc.name}`} shape="rect" placeholder="클랜 홍보 포스터 (3:4)를 끌어다 놓으세요" style={{ display: 'block', width: '100%', height: 480 }}></image-slot>
</div>
<div style={{ flex: 1, minWidth: 0, padding: '20px 24px 24px', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', border: '1px solid var(--line)', borderRadius: 7, overflow: 'hidden', marginBottom: 16 }}>
{[['랭킹', `${rc.rank}`, PZPT], ['승률', `${rc.weeklyWr}%`, 'var(--ink)'], ['전적', `${rc.record.matches}${rc.record.w}${rc.record.l}`, 'var(--ink)']].map(([l, v, c], i) => (
<div key={l} style={{ flex: i === 2 ? 1.6 : 1, minWidth: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 5, padding: '11px 4px', borderLeft: i ? '1px solid var(--line)' : 'none', background: i % 2 ? '#fff' : '#fafbfc' }}>
<div style={{ fontSize: 10, lineHeight: 1, color: 'var(--t3)', fontWeight: 600 }}>{l}</div>
<div style={{ fontFamily: PZCH, fontWeight: 700, fontSize: 13.5, lineHeight: 1, whiteSpace: 'nowrap', color: c }}>{v}</div>
</div>
))}
</div>
<div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--t3)', marginBottom: 6 }}>모집 안내</div>
<p style={{ margin: '0 0 14px', fontSize: 13.5, lineHeight: 1.7, color: '#3a4049', whiteSpace: 'pre-line' }}>{rc.intro}</p>
<div style={{ marginTop: 'auto', display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
{rc.recruitTypes.map((t) => (
<span key={t} style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--ink)', border: '1px solid var(--line2)', background: '#fff', padding: '3px 10px', borderRadius: 5 }}>{t}</span>
))}
</div>
<div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--t3)', marginBottom: 7 }}>가입 조건</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '7px 18px', marginBottom: 14, padding: '12px 14px', background: '#fafbfc', border: '1px solid var(--line)', borderRadius: 7 }}>
{(() => {
const cd = rc.conditions;
return [
['연령', pzAgeLabel(cd)],
['보이스 채팅', cd.voice ? '필수' : '자율'],
['닉네임 변경', cd.nickChange ? '필요' : '불필요'],
['이중클랜·부계정', cd.noDualClan ? '금지' : '허용'],
['지인 동반 가입', cd.friendJoin ? '가능' : '불가'],
].map(([l, v]) => (
<div key={l} style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
<span style={{ fontSize: 11.5, color: 'var(--t3)', fontWeight: 600, flexShrink: 0 }}>{l}</span>
<span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink)', textAlign: 'right' }}>{v}</span>
</div>
));
})()}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
<button onClick={() => onOpenClan(rc.name)} 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 style={{ flex: 1.3, padding: '11px 0', borderRadius: 7, border: 'none', background: PZPT, color: '#fff', fontWeight: 700, fontSize: 13.5, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>가입 신청</button>
{rc.contacts.map((ct) => {
const isDc = ct.type === '디스코드';
return (
<button key={ct.type} title={ct.label} aria-label={ct.label} style={{ width: 42, height: 42, flexShrink: 0, display: 'grid', placeItems: 'center', borderRadius: 7, border: 'none', cursor: 'pointer', background: isDc ? '#5865F2' : '#FEE500' }}>
<window.ContactGlyph type={ct.type} />
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}
// ── 모집글 등록 모달 ──
function RecruitCreate({ onClose }) {
const SA = window.SA;
const myClanName = SA.me.clan || '불곰부대';
const clan = (window.ClanDB && window.ClanDB(myClanName)) || {};
const [types, setTypes] = React.useState([]);
const [ageMin, setAgeMin] = React.useState('');
const [ageMax, setAgeMax] = React.useState('');
const [conds, setConds] = React.useState(['보이스 채팅 필수']);
const [intro, setIntro] = React.useState('');
const [dc, setDc] = React.useState(false);
const [dcLink, setDcLink] = React.useState('');
const [kakao, setKakao] = React.useState(false);
const [kakaoLink, setKakaoLink] = React.useState('');
const toggleType = (v) => setTypes((t) => t.includes(v) ? t.filter((x) => x !== v) : [...t, v]);
const toggleCond = (v) => setConds((t) => t.includes(v) ? t.filter((x) => x !== v) : [...t, v]);
const label = { fontSize: 12, fontWeight: 700, color: 'var(--t2)', marginBottom: 7 };
const chip = (on) => ({ padding: '6px 12px', borderRadius: 5, border: `1px solid ${on ? 'var(--ink)' : 'var(--line2)'}`, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, background: on ? 'var(--ink)' : '#fff', color: on ? '#fff' : 'var(--t2)' });
const valid = intro.trim().length > 0 && types.length > 0;
return (
<div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 250, background: 'rgba(20,22,26,.62)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 20px', overflowY: 'auto', animation: 'sgFade .2s ease' }}>
<div onClick={(e) => e.stopPropagation()} style={{ width: 760, maxWidth: '100%', margin: 'auto', background: '#fff', borderRadius: 14, overflow: 'hidden', boxShadow: '0 24px 70px rgba(0,0,0,.34)', fontFamily: "'Pretendard',sans-serif" }}>
{/* 헤더 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '15px 20px', borderBottom: '1px solid var(--line)' }}>
<div>
<div style={{ fontSize: 16.5, fontWeight: 800, color: 'var(--ink)' }}>모집글 등록</div>
<div style={{ fontSize: 11.5, color: 'var(--t3)', marginTop: 2 }}>랭킹 · 승률 · 전적은 클랜 데이터에서 자동으로 표시됩니다.</div>
</div>
<button onClick={onClose} style={{ width: 30, height: 30, borderRadius: 6, border: 'none', background: '#eef0f2', color: '#5b626c', cursor: 'pointer', fontSize: 16 }}>×</button>
</div>
<div style={{ display: 'flex', gap: 0, alignItems: 'stretch' }}>
{/* 포스터 업로드 */}
<div style={{ width: 300, flexShrink: 0, background: '#f4f5f7', borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column' }}>
<image-slot id={`recruit-poster-${myClanName}`} shape="rect" placeholder="홍보 포스터 (3:4) 업로드" style={{ display: 'block', width: '100%', height: 400 }}></image-slot>
<div style={{ padding: '11px 16px', color: 'var(--t3)', fontSize: 11, lineHeight: 1.5 }}>권장 비율 3:4 · 클랜을 가장 보여줄 포스터를 올려보세요.</div>
</div>
{/* 폼 */}
<div style={{ flex: 1, minWidth: 0, padding: '18px 22px 20px', maxHeight: 520, overflowY: 'auto' }}>
{/* 클랜 (자동) */}
<div style={{ marginBottom: 16 }}>
<div style={label}>클랜</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', border: '1px solid var(--line)', borderRadius: 7, background: '#fafbfc' }}>
<window.Emblem cfg={clan.emblem} size={28} />
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)' }}>{myClanName}</span>
<span style={{ marginLeft: 'auto', fontSize: 11.5, color: 'var(--t3)', fontFamily: PZCH }}>{clan.rank}</span>
</div>
</div>
{/* 모집 유형 */}
<div style={{ marginBottom: 16 }}>
<div style={label}>모집 유형 <span style={{ color: 'var(--t3)', fontWeight: 600 }}>(어떤 유저를 찾나요?)</span></div>
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
{(window.RECRUIT_TYPES || []).map((v) => {
const on = types.includes(v);
return (
<button key={v} onClick={() => toggleType(v)} style={chip(on)}>{v}</button>
);
})}
</div>
</div>
{/* 연령 조건 */}
<div style={{ marginBottom: 16 }}>
<div style={label}>연령 조건 <span style={{ color: 'var(--t3)', fontWeight: 600 }}>(비워두면 전연령)</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input type="number" min="1" max="99" value={ageMin} onChange={(e) => setAgeMin(e.target.value)} placeholder="제한 없음"
style={{ width: 90, padding: '9px 11px', border: '1px solid var(--line2)', borderRadius: 7, fontSize: 13, fontFamily: "'Pretendard',sans-serif", fontWeight: 600, color: 'var(--ink)', outline: 'none', boxSizing: 'border-box' }} />
<span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--t2)', flexShrink: 0 }}> 이상</span>
<span style={{ color: 'var(--line2)' }}>~</span>
<input type="number" min="1" max="99" value={ageMax} onChange={(e) => setAgeMax(e.target.value)} placeholder="제한 없음"
style={{ width: 90, padding: '9px 11px', border: '1px solid var(--line2)', borderRadius: 7, fontSize: 13, fontFamily: "'Pretendard',sans-serif", fontWeight: 600, color: 'var(--ink)', outline: 'none', boxSizing: 'border-box' }} />
<span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--t2)', flexShrink: 0 }}> 이하</span>
</div>
</div>
{/* 가입 조건 */}
<div style={{ marginBottom: 16 }}>
<div style={label}>가입 조건 <span style={{ color: 'var(--t3)', fontWeight: 600 }}>(해당하는 항목 선택)</span></div>
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
{['보이스 채팅 필수', '닉네임 변경 필요', '이중클랜·부계 금지', '지인 동반 가입 가능'].map((v) => (
<button key={v} onClick={() => toggleCond(v)} style={chip(conds.includes(v))}>{v}</button>
))}
</div>
</div>
{/* 모집 안내 */}
<div style={{ marginBottom: 16 }}>
<div style={label}>모집 안내</div>
<textarea value={intro} onChange={(e) => setIntro(e.target.value)} placeholder="클랜 분위기와 매너 기준, 활동 시간대, 클랜 목표(정기전·내전·티어 올리기 등)를 자유롭게 적어주세요." rows={5}
style={{ width: '100%', resize: 'vertical', padding: '11px 12px', border: '1px solid var(--line2)', borderRadius: 7, fontSize: 13.5, lineHeight: 1.6, fontFamily: "'Pretendard',sans-serif", color: '#3a4049', outline: 'none', boxSizing: 'border-box' }} />
</div>
{/* 문의 채널 (선택) */}
<div>
<div style={label}>문의 채널 <span style={{ color: 'var(--t3)', fontWeight: 600 }}>(선택)</span></div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
{[['디스코드', dc, () => setDc((v) => !v), dcLink, setDcLink, '#5865F2', 'https://discord.gg/...'], ['오픈카톡', kakao, () => setKakao((v) => !v), kakaoLink, setKakaoLink, '#FEE500', 'https://open.kakao.com/...']].map(([nm, on, fn, lk, setLk, bg, ph]) => (
<div key={nm} style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
<button onClick={fn} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '9px 12px', minWidth: 124, borderRadius: 7, cursor: 'pointer', fontWeight: 700, fontSize: 13, fontFamily: "'Pretendard',sans-serif", flexShrink: 0,
border: on ? `2px solid ${bg}` : '1px solid var(--line2)', background: on ? `${bg}14` : '#fff', color: on ? 'var(--ink)' : 'var(--t3)' }}>
<span style={{ width: 22, height: 22, borderRadius: 5, background: bg, display: 'grid', placeItems: 'center' }}><window.ContactGlyph type={nm} /></span>
{nm}
</button>
<input value={lk} onChange={(e) => setLk(e.target.value)} disabled={!on} placeholder={ph}
style={{ flex: 1, minWidth: 0, padding: '9px 11px', border: '1px solid var(--line2)', borderRadius: 7, fontSize: 12.5, fontFamily: "'Pretendard',sans-serif", color: 'var(--ink)', background: on ? '#fff' : '#f4f5f7', outline: 'none', boxSizing: 'border-box' }} />
</div>
))}
</div>
</div>
</div>
</div>
{/* 푸터 */}
<div style={{ display: 'flex', gap: 10, padding: '14px 20px', borderTop: '1px solid var(--line)', background: '#fafbfc' }}>
<button onClick={onClose} style={{ flex: 1, padding: '12px 0', borderRadius: 7, border: '1px solid var(--line2)', background: '#fff', color: '#3a4049', fontWeight: 700, fontSize: 14, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>취소</button>
<button onClick={onClose} disabled={!valid} style={{ flex: 2, padding: '12px 0', borderRadius: 7, border: 'none', background: valid ? PZPT : '#c4c8ce', color: '#fff', fontWeight: 700, fontSize: 14, cursor: valid ? 'pointer' : 'not-allowed', fontFamily: "'Pretendard',sans-serif" }}>모집글 등록하기</button>
</div>
</div>
</div>
);
}
function ClanPlaza({ onClose, onOpenClan, onLogin, onClan, onCommunity }) {
const all = window.ClanRecruits();
const [rtype, setRtype] = React.useState('전체');
const [showFilter, setShowFilter] = React.useState(false);
const [fAge, setFAge] = React.useState('');
const [fVoice, setFVoice] = React.useState('전체');
const [fNick, setFNick] = React.useState('전체');
const [fDual, setFDual] = React.useState('전체');
const [fFriend, setFFriend] = React.useState('전체');
const [detail, setDetail] = React.useState(null);
const [creating, setCreating] = React.useState(false);
const [page, setPage] = React.useState(1);
const PER_PAGE = 8;
const myClanName = window.SA.me.clan || '불곰부대';
const myPost = all.find((c) => c.name === myClanName);
const [myActive, setMyActive] = React.useState(() => { try { return localStorage.getItem('sa-my-recruit-active') !== '0'; } catch (e) { return true; } });
const toggleMy = () => setMyActive((v) => { const n = !v; try { localStorage.setItem('sa-my-recruit-active', n ? '1' : '0'); } catch (e) {} return n; });
let list = [...all];
if (rtype !== '전체') list = list.filter((c) => c.recruitTypes.includes(rtype));
if (fAge) { const a = parseInt(fAge, 10); if (!isNaN(a)) list = list.filter((c) => (c.conditions.ageMin == null || a >= c.conditions.ageMin) && (c.conditions.ageMax == null || a <= c.conditions.ageMax)); }
if (fVoice !== '전체') list = list.filter((c) => fVoice === '필수' ? c.conditions.voice : !c.conditions.voice);
if (fNick !== '전체') list = list.filter((c) => fNick === '불필요' ? !c.conditions.nickChange : c.conditions.nickChange);
if (fDual !== '전체') list = list.filter((c) => fDual === '허용' ? !c.conditions.noDualClan : c.conditions.noDualClan);
if (fFriend !== '전체') list = list.filter((c) => fFriend === '가능' ? c.conditions.friendJoin : !c.conditions.friendJoin);
if (!myActive) list = list.filter((c) => c.name !== myClanName);
list = [...list].sort((a, b) => b.hot - a.hot);
const totalPages = Math.max(1, Math.ceil(list.length / PER_PAGE));
const curPage = Math.min(page, totalPages);
const pageList = list.slice((curPage - 1) * PER_PAGE, curPage * PER_PAGE);
const goPage = (n) => { setPage(Math.max(1, Math.min(totalPages, n))); };
const setRtypeReset = (v) => { setRtype(v); setPage(1); };
const setF = (setter) => (v) => { setter(v); setPage(1); };
const activeFilters = (fAge ? 1 : 0) + (fVoice !== '전체' ? 1 : 0) + (fNick !== '전체' ? 1 : 0) + (fDual !== '전체' ? 1 : 0) + (fFriend !== '전체' ? 1 : 0);
const resetFilters = () => { setFAge(''); setFVoice('전체'); setFNick('전체'); setFDual('전체'); setFFriend('전체'); setPage(1); };
const totalOpenings = all.reduce((s, c) => s + c.openings, 0);
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'var(--bg2)', display: 'flex', flexDirection: 'column', animation: 'sgFade .22s ease' }}>
<window.Nav active="클랜" onLogin={onLogin} onClan={onClan} onCommunity={onCommunity} />
<div style={{ flex: 1, overflowY: 'auto' }}>
{/* 히어로 */}
<div style={{ background: 'linear-gradient(135deg,#23262b,#2e333b)', color: '#fff' }}>
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '30px 28px 26px' }}>
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 20, flexWrap: 'wrap' }}>
<div>
<div style={{ fontFamily: PZCH, fontSize: 11, fontWeight: 700, letterSpacing: '2.5px', color: 'rgba(255,255,255,.55)', marginBottom: 9 }}>CLAN RECRUIT PLAZA</div>
<h1 style={{ margin: 0, fontSize: 30, fontWeight: 800, letterSpacing: '-.6px' }}>클랜 모집 광장</h1>
<p style={{ margin: '8px 0 0', fontSize: 13.5, color: 'rgba(255,255,255,.7)' }}>
지금 <b style={{ color: '#fff', fontFamily: PZCH }}>{all.length}</b> 클랜이 <b style={{ color: '#fff', fontFamily: PZCH }}>{totalOpenings}</b>명의 식구를 찾고 있어요.
</p>
</div>
</div>
{/* 검색 + 내 클랜 가기 */}
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<div style={{ position: 'relative', flex: 1, minWidth: 240, maxWidth: 480 }}>
<span style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', display: 'flex' }}><Icon name="search" size={17} stroke="rgba(255,255,255,.55)" /></span>
<input placeholder="클랜명 · 분위기 · 포지션으로 검색" style={{ width: '100%', height: 42, paddingLeft: 40, paddingRight: 14, borderRadius: 8, border: '1px solid rgba(255,255,255,.2)', background: 'rgba(255,255,255,.1)', color: '#fff', fontSize: 14, outline: 'none', fontFamily: "'Pretendard',sans-serif" }} />
</div>
<button onClick={() => onOpenClan(window.SA.me.clan || '불곰부대')} style={{ height: 42, padding: '0 20px', borderRadius: 8, border: '1px solid rgba(255,255,255,.25)', background: 'rgba(255,255,255,.1)', color: '#fff', fontWeight: 700, fontSize: 13.5, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", flexShrink: 0, marginLeft: 'auto' }}>
클랜 가기
</button>
</div>
</div>
</div>
{/* 필터/정렬 바 */}
<div style={{ background: '#fff', borderBottom: '1px solid var(--line2)', position: 'sticky', top: 0, zIndex: 5 }}>
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '12px 28px', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
{['전체', ...(window.RECRUIT_TYPES || [])].map((v) => {
const on = v === rtype;
return (
<button key={v} onClick={() => setRtypeReset(v)} style={{ padding: '6px 12px', borderRadius: 20, border: `1px solid ${on ? PZPT : 'var(--line2)'}`, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12, background: on ? PZPT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{v}</button>
);
})}
</div>
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 12, color: 'var(--t3)', fontWeight: 600 }}>{list.length} 클랜</span>
<button onClick={() => setShowFilter((v) => !v)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '7px 12px', borderRadius: 6, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 12.5,
border: `1px solid ${showFilter || activeFilters ? PZPT : 'var(--line2)'}`, background: showFilter ? PZPT : '#fff', color: showFilter ? '#fff' : (activeFilters ? PZPT : 'var(--ink)') }}>
상세 필터{activeFilters > 0 && <span style={{ fontFamily: PZCH, fontSize: 11, fontWeight: 700, background: showFilter ? 'rgba(255,255,255,.25)' : `${PZPT}18`, borderRadius: 9, padding: '0 6px' }}>{activeFilters}</span>}
</button>
<button onClick={() => setCreating(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', borderRadius: 6, border: 'none', background: PZPT, color: '#fff', fontWeight: 700, fontSize: 12.5, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>
모집글 등록
</button>
</div>
</div>
{/* 상세 필터 행 */}
{showFilter && (
<div style={{ borderTop: '1px solid var(--line)', background: '#fafbfc' }}>
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '12px 28px', display: 'flex', alignItems: 'flex-end', gap: 18, flexWrap: 'wrap' }}>
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t3)', marginBottom: 6 }}> 나이</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input type="number" min="1" max="99" value={fAge} onChange={(e) => setF(setFAge)(e.target.value)} placeholder="나이"
style={{ width: 64, padding: '7px 9px', border: '1px solid var(--line2)', borderRadius: 6, fontSize: 12.5, fontFamily: "'Pretendard',sans-serif", fontWeight: 600, color: 'var(--ink)', outline: 'none', boxSizing: 'border-box', background: '#fff' }} />
<span style={{ fontSize: 11.5, color: 'var(--t3)', fontWeight: 600 }}> 가입 가능</span>
</div>
</div>
{[['보이스 채팅', fVoice, setFVoice, ['전체', '필수', '자율']], ['닉네임 변경', fNick, setFNick, ['전체', '불필요', '필요']], ['이중클랜·부계', fDual, setFDual, ['전체', '허용', '금지']], ['지인 동반', fFriend, setFFriend, ['전체', '가능', '불가']]].map(([lab, val, setter, opts]) => (
<div key={lab}>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t3)', marginBottom: 6 }}>{lab}</div>
<div style={{ display: 'flex', gap: 4 }}>
{opts.map((o) => {
const on = val === o;
return (
<button key={o} onClick={() => setF(setter)(o)} style={{ padding: '6px 11px', borderRadius: 6, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", fontWeight: 700, fontSize: 11.5,
border: `1px solid ${on ? PZPT : 'var(--line2)'}`, background: on ? PZPT : '#fff', color: on ? '#fff' : 'var(--t2)' }}>{o}</button>
);
})}
</div>
</div>
))}
{activeFilters > 0 && (
<button onClick={resetFilters} style={{ marginLeft: 'auto', padding: '7px 12px', borderRadius: 6, border: 'none', background: 'transparent', color: 'var(--t2)', fontWeight: 700, fontSize: 12, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif", textDecoration: 'underline' }}>초기화</button>
)}
</div>
</div>
)}
</div>
{/* 내 모집글 관리 */}
{myPost && (
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '16px 28px 0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, background: '#fff', border: `1px solid ${myActive ? 'var(--line)' : 'var(--line2)'}`, borderLeft: `3px solid ${myActive ? PZPT : '#c4c8ce'}`, borderRadius: 7, padding: '11px 16px', flexWrap: 'wrap' }}>
<window.Emblem cfg={myPost.emblem} size={26} />
<span style={{ fontSize: 13.5, fontWeight: 800, color: 'var(--ink)' }}>{myClanName}</span>
<span style={{ fontSize: 12, fontWeight: 700, color: myActive ? PZPT : 'var(--t3)' }}> 모집글 · {myActive ? '게시 중' : '비활성화됨'}</span>
{!myActive && <span style={{ fontSize: 11.5, color: 'var(--t3)' }}>목록에 노출되지 않아요.</span>}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
{myActive && <button onClick={() => setDetail(myPost)} style={{ padding: '7px 13px', borderRadius: 6, border: '1px solid var(--line2)', background: '#fff', color: '#3a4049', fontWeight: 700, fontSize: 12, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>미리보기</button>}
<button onClick={toggleMy} style={{ padding: '7px 13px', borderRadius: 6, border: 'none', background: myActive ? '#eef0f2' : PZPT, color: myActive ? '#5b626c' : '#fff', fontWeight: 700, fontSize: 12, cursor: 'pointer', fontFamily: "'Pretendard',sans-serif" }}>
{myActive ? '비활성화' : '다시 활성화'}
</button>
</div>
</div>
</div>
)}
{/* 카드 그리드 */}
<div style={{ maxWidth: 1180, margin: '0 auto', padding: '20px 28px 46px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 16 }}>
{pageList.map((rc) => <RecruitCard key={rc.name} rc={rc} onOpen={onOpenClan} onDetail={setDetail} />)}
</div>
{totalPages > 1 && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 26 }}>
<button onClick={() => goPage(curPage - 1)} disabled={curPage === 1} style={{ width: 32, height: 32, borderRadius: 6, border: '1px solid var(--line2)', background: '#fff', cursor: curPage === 1 ? 'default' : 'pointer', opacity: curPage === 1 ? .4 : 1, display: 'grid', placeItems: 'center' }}>
<Icon name="chevL" size={15} stroke="var(--t2)" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
<button key={n} onClick={() => goPage(n)} style={{ minWidth: 32, height: 32, padding: '0 6px', borderRadius: 6, cursor: 'pointer', fontFamily: "'Chakra Petch',sans-serif", fontWeight: 700, fontSize: 13,
border: n === curPage ? 'none' : '1px solid var(--line2)', background: n === curPage ? PZPT : '#fff', color: n === curPage ? '#fff' : 'var(--t2)' }}>{n}</button>
))}
<button onClick={() => goPage(curPage + 1)} disabled={curPage === totalPages} style={{ width: 32, height: 32, borderRadius: 6, border: '1px solid var(--line2)', background: '#fff', cursor: curPage === totalPages ? 'default' : 'pointer', opacity: curPage === totalPages ? .4 : 1, display: 'grid', placeItems: 'center' }}>
<Icon name="chevR" size={15} stroke="var(--t2)" />
</button>
</div>
)}
</div>
</div>
{detail && <RecruitDetail rc={detail} onClose={() => setDetail(null)} onOpenClan={onOpenClan} />}
{creating && <RecruitCreate onClose={() => setCreating(false)} />}
</div>
);
}
window.ClanPlaza = ClanPlaza;
window.ContactGlyph = ContactGlyph;