458 lines
36 KiB
React
458 lines
36 KiB
React
/* 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;
|