// ─── Color-소개팅 · Admin (desktop-first responsive) ───────────────
// AdminLogin, AdminShell (top + sidebar), AdminDashboard, AdminList, AdminDetail

const TierChip = ({ tier }) => (
  <span className={`tier-chip ${tier}`}>
    <span className="swatch" />
    {TIER_LABEL[tier]}
  </span>
);

const TIER_COLORS = {
  diamond: 'linear-gradient(135deg, #9adfff 0%, #c8b8ff 55%, #ffc6f0 100%)',
  black: '#161412', red: '#e23a2e', blue: '#2f6cf6', yellow: '#f5c419', white: '#ffffff',
};
const tierAvatarBg = (t) => ({
  diamond: 'linear-gradient(135deg, #9adfff 0%, #c8b8ff 50%, #ffc6f0 100%)',
  black: 'linear-gradient(135deg, #2a2522, #161412)',
  red: 'linear-gradient(135deg, #ef6b62, #c52a1f)',
  blue: 'linear-gradient(135deg, #5e90fa, #2451c9)',
  yellow: 'linear-gradient(135deg, #ffd95a, #d6a40b)',
  white: 'linear-gradient(135deg, #fff, #e2d6bd)',
}[t]);
const tierAvatarColor = (t) => (t === 'yellow' || t === 'white' || t === 'diamond' ? '#161412' : '#fff');

/** 회원 이름 클릭 → 상세 화면 이동 */
function MemberNameLink({ member, name, id, openDetail, style, stopPropagation = false, children }) {
  const memberId = id || member?.id || member?._id;
  const displayName = name ?? member?.name ?? '—';
  const canOpen = !!openDetail && !!memberId;
  if (!canOpen) {
    return <span style={style}>{children ?? displayName}</span>;
  }
  const onClick = (e) => {
    if (stopPropagation) {
      e.preventDefault();
      e.stopPropagation();
    }
    openDetail(memberId);
  };
  return (
    <button
      type="button"
      onClick={onClick}
      title={`${displayName} 상세 보기`}
      style={{
        padding: 0, margin: 0, border: 0, background: 'transparent',
        font: 'inherit', color: 'inherit', fontWeight: 'inherit',
        letterSpacing: 'inherit', cursor: 'pointer',
        textDecoration: 'underline', textDecorationColor: 'rgba(22,20,18,0.22)',
        textUnderlineOffset: 3,
        ...style,
      }}
    >
      {children ?? displayName}
    </button>
  );
}

const DEFAULT_TIER_ORDER = ['diamond', 'black', 'red', 'blue', 'yellow', 'white'];
/** data.jsx의 `window.TIERS`(배열). 다른 스크립트가 덮었을 때도 관리자 화면이 깨지지 않게 보정 */
function getTierOrder() {
  const t = typeof window !== 'undefined' ? window.TIERS : undefined;
  return Array.isArray(t) && t.length ? t : DEFAULT_TIER_ORDER;
}

/** 백엔드 `profile-options` EDUCATION_OPTIONS 와 동일 (관리자 기본 프로필 셀렉트) */
const ADMIN_EDUCATION_OPTIONS = [
  'SKY', '인서울 4년제', '수도권 4년제', '지방 4년제', '전문대 2/3년제', '고졸', '해외 대학', '기타',
];
// 로그인 (mobile-friendly centered card)
// ═══════════════════════════════════════════════════════════
function AdminLogin({ go, onLoggedIn }) {
  const [id, setId] = React.useState('');
  const [pw, setPw] = React.useState('');
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [forgotHint, setForgotHint] = React.useState(false);

  const doLogin = async () => {
    if (!id || !pw || loading) return;
    setLoading(true); setError(null);
    try {
      const res = await window.CSApi.login(id, pw);
      window.CSApi.setToken(res.token);
      if (onLoggedIn) await onLoggedIn(res.admin);
      go('admin-dashboard');
    } catch (err) {
      setError(err.message || '로그인에 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{
      minHeight: '100vh', background: '#161412', color: '#fff',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: '32px 16px', position: 'relative',
    }}>
      {/* bg gradient */}
      <div style={{
        position: 'absolute', top: 0, left: 0, right: 0, height: 320,
        background: 'radial-gradient(circle at 30% 0%, rgba(226,58,46,0.18), transparent 60%), radial-gradient(circle at 80% 20%, rgba(47,108,246,0.15), transparent 50%)',
        pointerEvents: 'none',
      }} />

      <div style={{
        position: 'absolute', top: 22, left: 28,
      }}>
        <button onClick={() => go('home')} style={{
          color: 'rgba(255,255,255,0.55)', fontSize: 12, fontFamily: 'var(--f-mono)',
          letterSpacing: '0.06em', textTransform: 'uppercase',
        }}>← 소개팅 화면</button>
      </div>

      <div style={{
        width: '100%', maxWidth: 420, position: 'relative', zIndex: 1,
      }}>
        <div style={{
          fontFamily: 'var(--f-mono)', fontSize: 11, letterSpacing: '0.12em',
          color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', marginBottom: 18,
        }}>관리자 콘솔</div>
        <h1 style={{
          margin: 0, fontSize: 48, lineHeight: 1.05, fontWeight: 700, letterSpacing: '-0.03em',
        }}>
          <span style={{
            backgroundImage: 'linear-gradient(91deg, #fff 30%, #e23a2e 55%, #f5c419 75%, #2f6cf6 100%)',
            WebkitBackgroundClip: 'text', backgroundClip: 'text', color: 'transparent',
          }}>Color</span>{' '}
          <span style={{ fontFamily: 'var(--f-serif)', fontStyle: 'italic', fontWeight: 400, color: 'rgba(255,255,255,0.7)' }}>
            운영 화면
          </span>
        </h1>
        <p style={{ marginTop: 16, color: 'rgba(255,255,255,0.55)', fontSize: 14.5, lineHeight: 1.55 }}>
          1인 운영자 전용 콘솔입니다. 외부 노출 없음.
        </p>

        <div style={{ marginTop: 44 }}>
          <div className="tiny" style={{ color: 'rgba(255,255,255,0.4)', marginBottom: 8 }}>아이디</div>
          <input
            value={id}
            onChange={e => setId(e.target.value)}
            autoComplete="username"
            style={{
              width: '100%', background: 'transparent', border: 0,
              borderBottom: '1px solid rgba(255,255,255,0.18)',
              padding: '10px 0', fontSize: 22, color: '#fff', letterSpacing: '-0.01em',
            }}
          />
        </div>
        <div style={{ marginTop: 28 }}>
          <div className="tiny" style={{ color: 'rgba(255,255,255,0.4)', marginBottom: 8 }}>비밀번호</div>
          <input
            type="password"
            value={pw}
            onChange={e => setPw(e.target.value)}
            autoComplete="current-password"
            placeholder="••••••••"
            style={{
              width: '100%', background: 'transparent', border: 0,
              borderBottom: '1px solid rgba(255,255,255,0.18)',
              padding: '10px 0', fontSize: 22, color: '#fff', letterSpacing: '0.2em',
            }}
          />
        </div>
        <div style={{ marginTop: 14, display: 'flex', justifyContent: 'space-between' }}>
          <label style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'rgba(255,255,255,0.5)', fontSize: 13 }}>
            <input type="checkbox" style={{ accentColor: '#fff' }} defaultChecked readOnly title="로그인 토큰은 브라우저에 저장됩니다" /> 자동 로그인
          </label>
          <button
            type="button"
            style={{ color: 'rgba(255,255,255,0.5)', fontSize: 13 }}
            onClick={() => setForgotHint((v) => !v)}
          >비밀번호 안내</button>
        </div>
        {forgotHint && (
          <div style={{
            marginTop: 12, padding: '10px 14px', borderRadius: 10,
            background: 'rgba(255,255,255,0.08)', color: 'rgba(255,255,255,0.75)',
            fontSize: 12, lineHeight: 1.5,
          }}>
            관리자 비밀번호는 서버의 <span style={{ fontFamily: 'var(--f-mono)' }}>.env</span> 또는 초기 시드 설정을 확인하세요. 이 화면에서는 재설정되지 않습니다.
          </div>
        )}

        {error && (
          <div style={{
            marginTop: 18, padding: '10px 14px', borderRadius: 10,
            background: 'rgba(226,58,46,0.15)', color: '#ffd6d2',
            fontSize: 13, border: '1px solid rgba(226,58,46,0.4)',
          }}>⚠️ {error}</div>
        )}
        <button onClick={doLogin} disabled={loading || !id || !pw} style={{
          marginTop: 40, width: '100%', height: 56, borderRadius: 999,
          background: '#fff', color: '#161412', fontSize: 16, fontWeight: 600,
          cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.6 : 1,
        }}>{loading ? '로그인 중…' : '로그인'}</button>

        <div style={{
          marginTop: 24, fontFamily: 'var(--f-mono)', fontSize: 10,
          letterSpacing: '0.08em', color: 'rgba(255,255,255,0.3)',
          textAlign: 'center', textTransform: 'uppercase',
        }}>
          v1.0.0
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
// Shell (top + sidebar)
// ═══════════════════════════════════════════════════════════
/**
 * 관리자 상단 새로고침 버튼.
 *  - 클릭 시 회전 애니메이션 → window.location.reload()
 *  - JWT 토큰은 localStorage 에 있으므로 새로고침해도 로그인은 유지됨.
 *  - PC/모바일 공용 (.adm-top 은 모바일에서도 그대로 노출됨)
 */
function AdminRefreshButton() {
  const [spinning, setSpinning] = React.useState(false);
  const onClick = () => {
    setSpinning(true);
    setTimeout(() => {
      try { window.location.reload(); }
      catch (_) { setSpinning(false); }
    }, 180);
  };
  return (
    <button
      onClick={onClick}
      aria-label="새로고침"
      title="새로고침"
      style={{
        width: 32, height: 32, borderRadius: 999,
        background: 'rgba(255,255,255,0.08)', color: '#fff',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        border: '1px solid rgba(255,255,255,0.12)',
        cursor: 'pointer', flexShrink: 0,
      }}
    >
      <svg
        width="16" height="16" viewBox="0 0 24 24" fill="none"
        style={{
          transition: 'transform 480ms ease',
          transform: spinning ? 'rotate(360deg)' : 'rotate(0deg)',
        }}
      >
        <path
          d="M4 12a8 8 0 0 1 13.66-5.66L20 8M20 4v4h-4M20 12a8 8 0 0 1-13.66 5.66L4 16M4 20v-4h4"
          stroke="currentColor" strokeWidth="2"
          strokeLinecap="round" strokeLinejoin="round"
        />
      </svg>
    </button>
  );
}

function AdminShell({ go, active, children, members, seenNewMemberIds = [] }) {
  const seen = seenNewMemberIds;
  const newCount = members.filter(m => m.daysAgo <= 5 && !seen.includes(m.id)).length;
  return (
    <div className="adminroot">
      {/* Top */}
      <div className="adm-top">
        <div className="adm-top-l">
          <div className="brand-row">
            <span className="word">Color</span>
            <span style={{ color: 'rgba(255,255,255,0.6)', fontWeight: 500, fontSize: 13 }}>· 관리자</span>
          </div>
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '4px 10px', background: 'rgba(255,255,255,0.06)',
            borderRadius: 999, fontFamily: 'var(--f-mono)', fontSize: 10,
            letterSpacing: '0.08em', color: 'rgba(255,255,255,0.6)',
          }}>
            <span style={{ width: 6, height: 6, borderRadius: 999, background: '#5ad17a' }} />
            연결됨 · v1.0.0
          </div>
        </div>
        <div className="adm-top-r">
          <div style={{ fontFamily: 'var(--f-mono)', fontSize: 11, letterSpacing: '0.06em' }}>
            오늘 기준
          </div>
          <AdminRefreshButton />
          <div className="who">
            <span className="av">운</span>
            <span style={{ color: '#fff' }}>운영자</span>
          </div>
          <button onClick={() => { window.CSApi.logout(); go('home'); }}>로그아웃</button>
        </div>
      </div>

      {/* Mobile tabs (visible <900px) */}
      <div className="adm-side-mobile-tabs">
        <button className={active === 'dashboard' ? 'on' : ''} onClick={() => go('admin-dashboard')}>
          대시보드
        </button>
        <button className={active === 'list' || active === 'detail' ? 'on' : ''} onClick={() => go('admin-list')}>
          회원 목록 {newCount > 0 && <span className="badge">{newCount}</span>}
        </button>
        <button onClick={() => go('admin-match-pick')}>매칭하기</button>
        <button className={active === 'match-history' ? 'on' : ''} onClick={() => go('admin-match-history')}>
          매칭 기록
        </button>
        <button onClick={() => go('admin-log')}>활동 로그</button>
        <button className={active === 'messages' ? 'on' : ''} onClick={() => go('admin-messages')}>
          문구 관리
        </button>
        <button className={active === 'insta-story' ? 'on' : ''} onClick={() => go('admin-instagram-story')}>
          스토리
        </button>
      </div>

      <div className="adm-body">
        {/* Sidebar */}
        <aside className="adm-side">
          <div className="group">업무</div>
          <button className={`nav-item ${active === 'dashboard' ? 'on' : ''}`} onClick={() => go('admin-dashboard')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <rect x="1.5" y="1.5" width="5.5" height="5.5" rx="1.5" stroke="currentColor" strokeWidth="1.4"/>
                <rect x="9" y="1.5" width="5.5" height="5.5" rx="1.5" stroke="currentColor" strokeWidth="1.4"/>
                <rect x="1.5" y="9" width="5.5" height="5.5" rx="1.5" stroke="currentColor" strokeWidth="1.4"/>
                <rect x="9" y="9" width="5.5" height="5.5" rx="1.5" stroke="currentColor" strokeWidth="1.4"/>
              </svg>
            </span>
            대시보드
          </button>
          <button className={`nav-item ${active === 'list' || active === 'detail' ? 'on' : ''}`} onClick={() => go('admin-list')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <circle cx="6" cy="5" r="2.5" stroke="currentColor" strokeWidth="1.4"/>
                <circle cx="11.5" cy="6" r="1.8" stroke="currentColor" strokeWidth="1.4"/>
                <path d="M1.5 13.5C2 11.5 4 10.5 6 10.5C8 10.5 10 11.5 10.5 13.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
                <path d="M11 11C12.5 11 14 11.8 14.5 13" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
              </svg>
            </span>
            회원 목록
            {newCount > 0 && <span className="badge">{newCount}</span>}
          </button>
          <button className={`nav-item ${active === 'match' ? 'on' : ''}`} onClick={() => go('admin-match-pick')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <path d="M8 14C8 14 1.5 10.5 1.5 6C1.5 3.5 3.3 2 5.5 2C6.8 2 7.6 2.8 8 3.6C8.4 2.8 9.2 2 10.5 2C12.7 2 14.5 3.5 14.5 6C14.5 10.5 8 14 8 14Z" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round"/>
              </svg>
            </span>
            매칭하기
          </button>
          <button className={`nav-item ${active === 'match-history' ? 'on' : ''}`} onClick={() => go('admin-match-history')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <circle cx="4" cy="4" r="1.3" stroke="currentColor" strokeWidth="1.3" />
                <circle cx="11.5" cy="4" r="1.3" stroke="currentColor" strokeWidth="1.3" />
                <circle cx="11.5" cy="12" r="1.3" stroke="currentColor" strokeWidth="1.3" />
                <path d="M4 5.3v6.7h6.2" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
                <path d="M5.3 4h4.9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
              </svg>
            </span>
            매칭 기록
          </button>
          <button className={`nav-item ${active === 'log' || active === 'log-detail' ? 'on' : ''}`} onClick={() => go('admin-log')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" strokeWidth="1.4"/>
                <path d="M5 6h6M5 8.5h6M5 11h4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
              </svg>
            </span>
            활동 로그
          </button>

          <button className={`nav-item ${active === 'messages' ? 'on' : ''}`} onClick={() => go('admin-messages')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <path d="M2 4a2 2 0 012-2h8a2 2 0 012 2v6a2 2 0 01-2 2H7l-3 2.5V12H4a2 2 0 01-2-2V4z" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round"/>
                <path d="M5.2 6.2h5.6M5.2 8.6h3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
              </svg>
            </span>
            문구 관리
          </button>

          <div className="group">인스타 컨텐츠 생성</div>
          <button className={`nav-item ${active === 'insta-story' ? 'on' : ''}`} onClick={() => go('admin-instagram-story')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <rect x="2" y="3" width="12" height="10" rx="2" stroke="currentColor" strokeWidth="1.4"/>
                <circle cx="8" cy="8" r="2.2" stroke="currentColor" strokeWidth="1.4"/>
                <path d="M5 2.5h6" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
              </svg>
            </span>
            스토리 생성
          </button>

          <div className="group">시스템</div>
          <button className={`nav-item ${active === 'settings' ? 'on' : ''}`} onClick={() => go('admin-settings')}>
            <span className="ico">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <circle cx="8" cy="8" r="2" stroke="currentColor" strokeWidth="1.4"/>
                <path d="M8 1V3M8 13V15M15 8H13M3 8H1M13 3L11.5 4.5M4.5 11.5L3 13M13 13L11.5 11.5M4.5 4.5L3 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
              </svg>
            </span>
            설정
          </button>

          <div className="spacer" />
          <div className="foot">v1.0.0 · 단일 운영 계정</div>
        </aside>

        {/* Main */}
        <main className="adm-main">
          {children}
        </main>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
// 대시보드
// ═══════════════════════════════════════════════════════════
function dashFmtAsOf(iso) {
  if (!iso) return '—';
  try {
    return new Date(iso).toLocaleString('ko-KR', {
      year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
    });
  } catch {
    return '—';
  }
}

const EMPTY_FUNNEL = [
  { label: '신청', count: 0 },
  { label: '검토·준비', count: 0 },
  { label: '매칭 연결', count: 0 },
  { label: '만남 조율', count: 0 },
  { label: '만남 완료', count: 0 },
];

function AdminDashboard({ go, members, dashboard, seenNewMemberIds = [], openDetail }) {
  const d = dashboard && dashboard.ok !== false ? dashboard : {};
  const unseenFromMembers = members.filter(m => m.daysAgo <= 5 && !seenNewMemberIds.includes(m.id)).length;
  const newCountDb = typeof d.newCount === 'number' ? d.newCount : null;
  const newCount = members.length > 0 ? unseenFromMembers : (newCountDb != null ? newCountDb : 0);

  const totalApps = typeof d.totalApplications === 'number' ? d.totalApplications : members.length;
  const totalMatches = typeof d.totalMatches === 'number' ? d.totalMatches : 0;
  const totalMembers = typeof d.totalMembers === 'number' ? d.totalMembers : members.length;
  const d7 = d.delta7d && typeof d.delta7d.applications === 'number'
    ? d.delta7d
    : { applications: 0, matches: 0 };

  const weekly = Array.isArray(d.weekly) && d.weekly.length ? d.weekly : (window.MATCH_HISTORY || []);
  const tierDist = Array.isArray(d.tierDist) && d.tierDist.length ? d.tierDist : [];
  const funnelSteps = Array.isArray(d.funnel) && d.funnel.length ? d.funnel : EMPTY_FUNNEL;

  const recentMembers = (Array.isArray(d.recent) && d.recent.length
    ? d.recent.map(r => window.normalizeMember(r)).filter(Boolean)
    : members.slice(0, 6));

  const asOf = dashFmtAsOf(d.generatedAt);

  return (
    <AdminShell go={go} active="dashboard" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">
            <span style={{ width: 6, height: 6, borderRadius: 999, background: '#ec4438' }} />
            홈 요약 · {asOf} 기준
          </div>
          <h1>어서오세요, <span style={{ fontFamily: 'var(--f-serif)', fontStyle: 'italic', fontWeight: 400 }}>운영자</span>님.</h1>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button
            type="button"
            className="btn ghost sm"
            onClick={() => window.downloadMembersCsv?.(members, 'dashboard-members')}
          >목록 저장 (CSV)</button>
          <button className="btn primary sm" onClick={() => go('admin-list')}>회원 목록 →</button>
        </div>
      </div>

      {/* KPI Grid */}
      <div className="adm-grid-4">
        <KPI
          dark
          label="최근 5일 신규"
          value={newCount}
          unit="건"
          sub={newCount > 0 ? '목록에서 아직 열어보지 않은 신규' : '확인 대기 중인 신규 없음'}
          right={newCount > 0 ? <span className="new-badge">NEW</span> : null}
        />
        <KPI label="누적 신청" value={totalApps.toLocaleString()} unit="건" sub={`최근 7일 +${d7.applications}건`} subClass="up" />
        <KPI label="누적 매칭" value={totalMatches.toLocaleString()} unit="건" sub={`최근 7일 +${d7.matches}건`} subClass="brand" accent="#ec4438" />
        <KPI label="등록 회원" value={totalMembers.toLocaleString()} unit="명" sub={`최근 7일 신규 +${d7.applications}명`} subClass="up" />
      </div>

      {/* Chart + tier dist */}
      <div className="adm-grid-2-1" style={{ marginTop: 16 }}>
        <div className="adm-card chart-card">
          <div className="chart-head">
            <div>
              <div className="tiny">최근 8주</div>
              <div className="chart-title">주간 신규 신청·신규 매칭</div>
              <div className="tiny" style={{ marginTop: 8, textTransform: 'none', letterSpacing: 0, lineHeight: 1.45, maxWidth: 520 }}>
                한 주마다 막대 <b>2개</b>: <b>왼쪽 검정</b> = 그 주에 들어온 <b>신규 접수</b> 건수, <b>오른쪽 빨강</b> = 그 주에 확정된 <b>신규 매칭</b> 건수입니다. 숫자는 막대 위에 표시됩니다.
              </div>
            </div>
            <div className="chart-legend">
              <span><span className="dot" style={{ background: '#161412' }} />신규 신청</span>
              <span><span className="dot" style={{ background: '#ec4438' }} />신규 매칭</span>
            </div>
          </div>
          <BarChart data={weekly} />
        </div>

        <div className="adm-card">
          <div className="tiny">등급(색상) 비율</div>
          <div className="chart-title" style={{ marginBottom: 12 }}>
            전체 {totalMembers.toLocaleString()}명
          </div>
          <TierBar tierDist={tierDist} />
          <div className="tier-dist-row" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))' }}>
            {getTierOrder().map((tier) => {
              const row = tierDist.find(x => x.tier === tier);
              const count = row ? row.count : 0;
              const pct = row ? row.pct : 0;
              return (
                <div key={tier} className="tier-dist-cell">
                  <TierChip tier={tier} />
                  <div style={{ fontSize: 18, fontWeight: 700, marginTop: 8 }}>{count}</div>
                  <div className="tiny">{pct}%</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>

      {/* Recent activity + funnel */}
      <div className="adm-grid-2" style={{ marginTop: 16 }}>
        <div className="adm-card">
          <div className="chart-head" style={{ marginBottom: 14 }}>
            <div>
              <div className="tiny">최근 신청 6건</div>
              <div className="chart-title">바로 확인할 신청</div>
            </div>
            <button onClick={() => go('admin-list')} style={{ fontSize: 12, color: 'var(--ink-500)' }}>전체 보기 →</button>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            {recentMembers.map(m => (
              <button key={m.id} type="button" onClick={() => openDetail?.(m.id)} style={{
                display: 'flex', alignItems: 'center', gap: 12,
                padding: '10px 6px', borderRadius: 10, textAlign: 'left',
                transition: 'background 100ms ease',
              }}
                onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-soft)'}
                onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
              >
                <div className="av" style={{
                  width: 34, height: 34, borderRadius: 10, background: tierAvatarBg(m.tier),
                  color: tierAvatarColor(m.tier), display: 'flex', alignItems: 'center', justifyContent: 'center',
                  fontWeight: 700, fontSize: 14, border: m.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
                }}>{m.name.charAt(0)}</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600 }}>
                    <MemberNameLink member={m} openDetail={openDetail} stopPropagation style={{ fontWeight: 600 }} />
                    <span style={{ color: 'var(--ink-500)', fontWeight: 400, fontSize: 12.5 }}>{m.age}, {m.gender === 'M' ? '남' : '여'}</span>
                    {m.daysAgo <= 5 && !seenNewMemberIds.includes(m.id) && <span className="new-badge">NEW</span>}
                  </div>
                  <div style={{ fontSize: 12, color: 'var(--ink-500)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {m.job}
                  </div>
                </div>
                <TierChip tier={m.tier} />
                <span style={{ fontFamily: 'var(--f-mono)', fontSize: 11, color: 'var(--ink-400)', width: 50, textAlign: 'right' }}>
                  {m.daysAgo}일
                </span>
              </button>
            ))}
          </div>
        </div>

        <div className="adm-card">
          <div className="tiny">단계별 현황</div>
          <div className="chart-title" style={{ marginBottom: 16 }}>신청부터 만남 완료까지</div>
          <Funnel steps={funnelSteps} />
        </div>
      </div>
    </AdminShell>
  );
}

function KPI({ label, value, unit, sub, subClass, right, accent, dark }) {
  return (
    <div className={`adm-card ${dark ? 'dark' : ''}`} style={accent && !dark ? { borderLeft: `3px solid ${accent}` } : {}}>
      <div className="kpi-label">
        <span className="lbl">{label}</span>
        {right}
      </div>
      <div className="kpi-value">
        <span className="num">{value}</span>
        <span className="unit">{unit}</span>
      </div>
      {sub && <div className={`kpi-sub ${subClass || ''}`}>{sub}</div>}
    </div>
  );
}

function BarChart({ data }) {
  const raw = Array.isArray(data) ? data : [];
  const norm = raw.map((d) => ({
    week: d.week != null ? String(d.week) : '—',
    applies: Math.max(0, Number(d.applies ?? d.application ?? 0) || 0),
    matches: Math.max(0, Number(d.matches ?? d.match ?? 0) || 0),
  }));
  const max = Math.max(1, ...norm.map((d) => Math.max(d.applies, d.matches)));
  const BAR_MAX = 132;

  if (!norm.length) {
    return (
      <div style={{ marginTop: 20, padding: '28px 12px', textAlign: 'center', color: 'var(--ink-500)', fontSize: 13 }}>
        아직 주간 집계 데이터가 없습니다.
      </div>
    );
  }

  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${norm.length}, minmax(0, 1fr))`,
        gap: 10,
        marginTop: 14,
        paddingBottom: 6,
      }}
    >
      {norm.map((d, i) => {
        const hA = (d.applies / max) * BAR_MAX;
        const hM = (d.matches / max) * BAR_MAX;
        const minVis = 3;
        return (
          <div
            key={`${d.week}-${i}`}
            style={{
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              minWidth: 0,
            }}
          >
            <div
              style={{
                display: 'flex',
                gap: 5,
                alignItems: 'flex-end',
                justifyContent: 'center',
                height: BAR_MAX + 26,
                width: '100%',
              }}
            >
              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '47%' }}>
                <span style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-500)', minHeight: 16, lineHeight: '16px' }}>
                  {d.applies}
                </span>
                <div
                  style={{
                    width: '100%',
                    height: d.applies > 0 ? Math.max(hA, minVis) : 0,
                    marginTop: 4,
                    background: '#161412',
                    borderRadius: '6px 6px 3px 3px',
                  }}
                  title={`신규 신청 ${d.applies}건`}
                />
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '47%' }}>
                <span style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-500)', minHeight: 16, lineHeight: '16px' }}>
                  {d.matches}
                </span>
                <div
                  style={{
                    width: '100%',
                    height: d.matches > 0 ? Math.max(hM, minVis) : 0,
                    marginTop: 4,
                    background: '#ec4438',
                    borderRadius: '6px 6px 3px 3px',
                  }}
                  title={`신규 매칭 ${d.matches}건`}
                />
              </div>
            </div>
            <div
              style={{
                marginTop: 8,
                fontFamily: 'var(--f-mono)',
                fontSize: 10,
                color: 'var(--ink-400)',
                letterSpacing: '0.04em',
                textAlign: 'center',
                whiteSpace: 'nowrap',
                overflow: 'hidden',
                textOverflow: 'ellipsis',
                width: '100%',
              }}
            >
              {d.week}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function TierBar({ tierDist }) {
  const order = getTierOrder();
  const rows = order.map(tier => {
    const row = (tierDist || []).find(x => x.tier === tier);
    const pct = row != null ? Number(row.pct) : 0;
    return { tier, pct, color: TIER_COLORS[tier] };
  });
  const sum = rows.reduce((s, t) => s + Math.max(t.pct, 0), 0);
  const allZero = sum < 0.05;
  return (
    <div style={{ display: 'flex', height: 32, borderRadius: 8, overflow: 'hidden', gap: 2 }}>
      {rows.map(t => (
        <div key={t.tier} style={{
          flex: allZero ? 1 : Math.max(t.pct, 0.8),
          background: t.color, position: 'relative',
          border: t.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 10, fontFamily: 'var(--f-mono)', fontWeight: 600,
          color: t.tier === 'yellow' || t.tier === 'white' || t.tier === 'diamond' ? '#161412' : '#fff',
        }}>{allZero ? '—' : `${t.pct}%`}</div>
      ))}
    </div>
  );
}

function Funnel({ steps }) {
  const base = Math.max(1, steps[0]?.count || 0);
  const derived = (steps || []).map(s => ({
    ...s,
    pct: Math.min(100, Math.round(((s.count || 0) / base) * 100)),
  }));
  const colors = ['#161412', '#34302a', '#7a5a4a', '#c52a1f', '#ec4438'];
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      {derived.map((s, i) => (
        <div key={s.label} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ width: 92, fontSize: 12.5, color: 'var(--ink-700)' }}>{s.label}</div>
          <div style={{ flex: 1, position: 'relative', height: 28, background: 'var(--bg-soft)', borderRadius: 6, overflow: 'hidden' }}>
            <div style={{
              position: 'absolute', left: 0, top: 0, bottom: 0,
              width: `${s.pct}%`, background: colors[i] || '#161412', borderRadius: 6,
              animation: `barIn 700ms ${i * 100}ms backwards ease-out`,
              display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
              paddingRight: 10, color: '#fff', fontSize: 11, fontFamily: 'var(--f-mono)', fontWeight: 600,
            }}>{s.pct}%</div>
          </div>
          <div style={{ width: 60, textAlign: 'right', fontSize: 13, fontWeight: 600, fontFamily: 'var(--f-mono)' }}>
            {(s.count || 0).toLocaleString()}
          </div>
        </div>
      ))}
      <style>{`@keyframes barIn { from { width: 0; } }`}</style>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
// 회원 목록
// ═══════════════════════════════════════════════════════════
function AdminList({ go, members, openDetail, startMatch, seenNewMemberIds = [], onMarkAllNewSeen }) {
  const [gender, setGender] = React.useState('all');
  const [tier, setTier] = React.useState('all');
  const [q, setQ] = React.useState('');
  const [sort, setSort] = React.useState('new');

  const filtered = React.useMemo(() => {
    let list = members.slice();
    if (gender !== 'all') list = list.filter(m => m.gender === gender);
    if (tier !== 'all') list = list.filter(m => m.tier === tier);
    if (q) {
      const ql = q.toLowerCase();
      list = list.filter(m =>
        m.name.toLowerCase().includes(ql) ||
        m.job.toLowerCase().includes(ql) ||
        m.region.toLowerCase().includes(ql) ||
        (m.workplace || '').toLowerCase().includes(ql) ||
        m.id.toLowerCase().includes(ql)
      );
    }
    if (sort === 'new') list.sort((a, b) => a.daysAgo - b.daysAgo);
    else if (sort === 'tier') list.sort((a, b) => TIER_RANK[b.tier] - TIER_RANK[a.tier]);
    else if (sort === 'age') list.sort((a, b) => a.age - b.age);
    return list;
  }, [members, gender, tier, q, sort]);

  return (
    <AdminShell go={go} active="list" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">회원 목록</div>
          <h1>회원 {filtered.length}<span style={{ fontSize: 18, color: 'var(--ink-500)', fontWeight: 500, marginLeft: 6 }}>/ {members.length}</span></h1>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button
            type="button"
            className="btn ghost sm"
            onClick={() => onMarkAllNewSeen?.()}
          >전체 읽음</button>
          <button
            type="button"
            className="btn ghost sm"
            onClick={() => window.downloadMembersCsv?.(filtered, 'member-list')}
          >목록 저장 (CSV)</button>
          <button
            type="button"
            className="btn primary sm"
            onClick={() => window.__adminNotify?.('수동 접수', '신청은 사용자 화면(/match/submit)에서만 받습니다.', 3600)}
          >+ 수동 추가</button>
        </div>
      </div>

      {/* Filter bar */}
      <div className="filter-bar">
        <div className="search">
          <span style={{ color: 'var(--ink-400)' }}>⌕</span>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="이름 · 직업 · 지역 · ID 검색" />
          {q && <button onClick={() => setQ('')} style={{ color: 'var(--ink-400)', fontSize: 14 }}>×</button>}
        </div>
        <div className="seg">
          {[{ v: 'all', l: '전체' }, { v: 'M', l: '남자' }, { v: 'F', l: '여자' }].map(g => (
            <button key={g.v} className={gender === g.v ? 'on' : ''} onClick={() => setGender(g.v)}>{g.l}</button>
          ))}
        </div>
        <div className="tier-pills">
          {[
            { v: 'all', l: '모든 등급', dot: null },
            { v: 'diamond', l: '다이아몬드', dot: 'linear-gradient(135deg, #9adfff, #c8b8ff, #ffc6f0)' },
            { v: 'black', l: '블랙', dot: '#161412' },
            { v: 'red', l: '레드', dot: '#e23a2e' },
            { v: 'blue', l: '블루', dot: '#2f6cf6' },
            { v: 'yellow', l: '옐로', dot: '#f5c419' },
            { v: 'white', l: '화이트', dot: '#fff' },
          ].map(t => (
            <button key={t.v} className={`tier-pill ${tier === t.v ? 'on' : ''}`} onClick={() => setTier(t.v)}>
              {t.dot && (
                <span className="dot" style={{ background: t.dot, borderColor: t.dot === '#fff' ? 'var(--ink-300)' : 'rgba(0,0,0,0.1)' }} />
              )}
              {t.l}
            </button>
          ))}
        </div>
        <div className="seg">
          {[
            { v: 'new', l: '최신순' },
            { v: 'tier', l: '등급순' },
            { v: 'age', l: '나이순' },
          ].map(s => (
            <button key={s.v} className={sort === s.v ? 'on' : ''} onClick={() => setSort(s.v)}>↕ {s.l}</button>
          ))}
        </div>
      </div>

      {/* Desktop table */}
      <div className="tbl-wrap">
        <table className="tbl">
          <thead>
            <tr>
              <th style={{ width: '24%' }}>회원</th>
              <th>나이/성별</th>
              <th>직업</th>
              <th>거주지</th>
              <th>MBTI</th>
              <th>등급</th>
              <th>신청일</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {filtered.map(m => (
              <tr key={m.id} onClick={() => openDetail(m.id)}>
                <td>
                  <div className="col-name">
                    <div className="av" style={{
                      background: tierAvatarBg(m.tier), color: tierAvatarColor(m.tier),
                      border: m.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
                    }}>{m.name.charAt(0)}</div>
                    <div>
                      <div className="av-name">
                        <MemberNameLink member={m} openDetail={openDetail} stopPropagation />
                        {m.daysAgo <= 5 && !seenNewMemberIds.includes(m.id) && <span className="new-badge">NEW</span>}
                      </div>
                      <div className="meta" style={{ fontFamily: 'var(--f-mono)' }}>{m.id}</div>
                    </div>
                  </div>
                </td>
                <td>
                  <span style={{ fontWeight: 600 }}>{m.age}</span>
                  <span style={{ color: 'var(--ink-500)', marginLeft: 4 }}>· {m.gender === 'M' ? '남' : '여'}</span>
                </td>
                <td style={{ maxWidth: 220 }}>
                  <div style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.job}</div>
                </td>
                <td style={{ color: 'var(--ink-700)' }}>{m.region.split(' ').slice(0, 2).join(' ')}</td>
                <td style={{ fontFamily: 'var(--f-mono)', fontSize: 12.5, fontWeight: 600 }}>{m.mbti}</td>
                <td><TierChip tier={m.tier} /></td>
                <td style={{ color: 'var(--ink-500)', fontFamily: 'var(--f-mono)', fontSize: 12 }}>
                  {m.daysAgo}일 전
                </td>
                <td style={{ color: 'var(--ink-300)', textAlign: 'right' }}>›</td>
              </tr>
            ))}
          </tbody>
        </table>

        {/* Mobile card list */}
        <div className="adm-mobile-list">
          {filtered.map(m => (
            <MobileRow key={m.id} m={m} onClick={() => openDetail(m.id)} seenNewMemberIds={seenNewMemberIds} openDetail={openDetail} />
          ))}
        </div>

        {filtered.length === 0 && (
          <div style={{ padding: 60, textAlign: 'center', color: 'var(--ink-400)', fontSize: 14 }}>
            조건에 맞는 회원이 없어요.
          </div>
        )}
      </div>
    </AdminShell>
  );
}

function MobileRow({ m, onClick, seenNewMemberIds = [], openDetail }) {
  return (
    <button onClick={onClick} style={{
      width: '100%', display: 'flex', alignItems: 'center', gap: 12,
      background: '#fff', border: '1px solid var(--line)',
      borderRadius: 16, padding: '12px 14px', textAlign: 'left',
    }}>
      <div className="av" style={{
        width: 48, height: 48, borderRadius: 14,
        background: tierAvatarBg(m.tier), color: tierAvatarColor(m.tier),
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: 18, fontWeight: 700, flexShrink: 0,
        border: m.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
      }}>{m.name.charAt(0)}</div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
          <MemberNameLink member={m} openDetail={openDetail} stopPropagation style={{ fontSize: 15, fontWeight: 700 }} />
          <span style={{ fontSize: 13, color: 'var(--ink-500)' }}>{m.age} · {m.gender === 'M' ? '남' : '여'}</span>
          {m.daysAgo <= 5 && !seenNewMemberIds.includes(m.id) && <span className="new-badge">NEW</span>}
        </div>
        <div style={{ fontSize: 12.5, color: 'var(--ink-500)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {m.job}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 6 }}>
          <TierChip tier={m.tier} />
          <span style={{ fontSize: 10, color: 'var(--ink-400)', fontFamily: 'var(--f-mono)' }}>{m.daysAgo}일 전</span>
        </div>
      </div>
      <div style={{ color: 'var(--ink-300)' }}>›</div>
    </button>
  );
}

// ── 회원 제출 사진 일괄 다운로드 (공통) ─────────────────────────────
/** 파일명에 쓸 문자열 정리 (경로·금지문자 제거, 공백은 한 칸으로) */
function sanitizePhotoFilenamePart(s) {
  return String(s ?? '')
    .trim()
    .replace(/[\\/:*?"<>|]/g, '_')
    .replace(/\s+/g, ' ');
}

/**
 * 제출 사진 전부 저장. 파일명: `이름 - 나이 - 회원번호 - 1.jpg` …
 * JSZip 있으면 zip 한 번, 없으면 순차 개별 다운로드.
 */
async function downloadMemberProfilePhotos(member, options = {}) {
  const notify = options.notify || window.__adminNotify;
  const photos = (member && member.photos ? member.photos : []).filter((p) => p && p.url);
  if (!photos.length) {
    notify?.('사진 없음', `${(member && member.name) || '회원'}님 제출 사진이 없습니다.`, 2400);
    return { ok: false, count: 0 };
  }
  const name = sanitizePhotoFilenamePart(member.name) || '회원';
  const age = member.age != null ? String(member.age) : '';
  const code = sanitizePhotoFilenamePart(member.memberCode || member.id || '');
  const base = [name, age, code].filter(Boolean).join(' - ');
  const items = [];
  for (let i = 0; i < photos.length; i++) {
    const p = photos[i];
    const res = await fetch(p.url, { cache: 'no-store' });
    if (!res.ok) throw new Error(`사진 ${i + 1} 다운로드 실패 (${res.status})`);
    const blob = await res.blob();
    const ext = (p.mimeType || blob.type || 'image/jpeg').split('/')[1]?.split('+')[0] || 'jpg';
    items.push({ name: `${base} - ${i + 1}.${ext}`, blob });
  }
  if (typeof window.JSZip === 'function') {
    const zip = new window.JSZip();
    for (const it of items) zip.file(it.name, it.blob);
    const zipBlob = await zip.generateAsync({ type: 'blob' });
    const url = URL.createObjectURL(zipBlob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${base}.zip`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 500);
    notify?.('저장 완료', `${items.length}장 · ${base}.zip`, 2400);
  } else {
    for (const it of items) {
      const url = URL.createObjectURL(it.blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = it.name;
      document.body.appendChild(a);
      a.click();
      a.remove();
      await new Promise((r) => setTimeout(r, 280));
      URL.revokeObjectURL(url);
    }
    notify?.('저장 완료', `${items.length}장 개별 다운로드`, 2400);
  }
  return { ok: true, count: items.length };
}

/** 사진 일괄 저장 + 프로필 모두 다운로드(동일 동작) — compact 는 트리/리스트용 작은 버튼 */
function MemberProfilePhotoDownloadButtons({ member, compact }) {
  const [busy, setBusy] = React.useState(false);
  const n = (member && member.photos) || [];
  const cnt = n.filter((p) => p && p.url).length;
  const run = async (e) => {
    e?.stopPropagation();
    e?.preventDefault();
    if (busy || !cnt) return;
    setBusy(true);
    try {
      await downloadMemberProfilePhotos(member, { notify: window.__adminNotify });
    } catch (err) {
      console.error(err);
      window.__adminNotify?.('다운로드 실패', err?.message || '네트워크 또는 CORS를 확인해주세요.', 3200);
    } finally {
      setBusy(false);
    }
  };
  const baseBtn = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: 4,
    borderRadius: 999,
    fontWeight: 700,
    cursor: cnt && !busy ? 'pointer' : 'not-allowed',
    border: 0,
    opacity: cnt ? 1 : 0.45,
    flexShrink: 0,
  };
  const sm = compact
    ? { fontSize: 10, padding: '4px 9px' }
    : { fontSize: 11.5, padding: '6px 12px' };
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }} onClick={(e) => e.stopPropagation()}>
      <button
        type="button"
        disabled={busy || !cnt}
        onClick={run}
        title={cnt ? '파일명: 이름 - 나이 - 회원번호 - 1 …' : '제출 사진 없음'}
        style={{
          ...baseBtn,
          ...sm,
          background: busy ? 'var(--bg-soft)' : '#161412',
          color: busy ? 'var(--ink-500)' : '#fff',
        }}
      >{busy ? '저장 중…' : `사진 일괄 저장${cnt ? ` (${cnt})` : ''}`}</button>
      <button
        type="button"
        disabled={busy || !cnt}
        onClick={run}
        title="제출 프로필 사진 전부(동일 규칙)"
        style={{
          ...baseBtn,
          ...sm,
          background: busy ? 'var(--bg-soft)' : '#fff',
          color: busy ? 'var(--ink-500)' : '#161412',
          border: '1px solid var(--line)',
        }}
      >{busy ? '…' : '프로필 모두 다운로드'}</button>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
// 회원 상세 · 기본 프로필 편집
// ═══════════════════════════════════════════════════════════
function mkBasicProfileDraft(member) {
  const y = new Date().getFullYear();
  return {
    age: typeof member.age === 'number' ? member.age : (member.birthYear ? y - member.birthYear : ''),
    gender: member.gender === 'F' ? 'F' : 'M',
    name: member.name || '',
    role: member.role || '',
    company: member.company || '',
    height: member.height ?? '',
    weight: member.weight ?? '',
    maritalStatus: member.maritalStatus === '돌싱' ? '돌싱' : '미혼',
    contact: String(member.contact || '').replace(/\D/g, ''),
    kakao: member.kakao || '',
    insta: member.insta || '',
    region: member.region || '',
    workplace: member.workplace || '',
    smoke: member.smoke || '비흡연자',
    school: member.school || '지방 4년제',
    salary: member.salary || '',
    car: member.car === '없음' ? '없음' : '있음',
    marry: member.marry || 'O',
    mbti: String(member.mbti || '').toUpperCase(),
  };
}

function MemberBasicProfileEditor({ member, onPersist }) {
  const [draft, setDraft] = React.useState(() => mkBasicProfileDraft(member));
  const [err, setErr] = React.useState(null);
  const [saving, setSaving] = React.useState(false);

  React.useEffect(() => {
    setDraft(mkBasicProfileDraft(member));
    setErr(null);
  }, [member]);

  const set = (patch) => { setDraft((d) => ({ ...d, ...patch })); };

  const schoolOpts = React.useMemo(() => {
    const s = draft.school;
    const base = [...ADMIN_EDUCATION_OPTIONS];
    if (s && !base.includes(s)) base.push(s);
    return base;
  }, [draft.school]);

  const save = async () => {
    setErr(null);
    const yearNow = new Date().getFullYear();
    const ageNum = parseInt(String(draft.age), 10);
    if (!Number.isFinite(ageNum) || ageNum < 18 || ageNum > 85) {
      setErr('나이는 만 18–85세 범위의 숫자로 입력해 주세요.');
      return;
    }
    const birthYear = yearNow - ageNum;
    if (birthYear < 1940 || birthYear > yearNow - 15) {
      setErr('출생연도 결과가 허용 범위를 벗어났습니다.');
      return;
    }
    const digits = String(draft.contact || '').replace(/\D/g, '');
    if (!/^01\d{9}$/.test(digits)) {
      setErr('연락처는 하이픈 없이 010으로 시작하는 11자리만 가능합니다.');
      return;
    }
    const ht = typeof draft.height === 'number'
      ? draft.height
      : parseInt(String(draft.height ?? '').trim(), 10);
    const wt = typeof draft.weight === 'number'
      ? draft.weight
      : parseInt(String(draft.weight ?? '').trim(), 10);
    if (!Number.isFinite(ht) || ht < 120 || ht > 230) {
      setErr('키는 120–230(cm) 숫자로 입력해 주세요.');
      return;
    }
    if (!Number.isFinite(wt) || wt < 30 || wt > 200) {
      setErr('몸무게는 30–200(kg) 숫자로 입력해 주세요.');
      return;
    }
    if (!String(draft.name || '').trim()) {
      setErr('이름을 입력해 주세요.');
      return;
    }

    const payload = {
      name: String(draft.name).trim(),
      birthYear,
      gender: draft.gender,
      role: String(draft.role).trim(),
      company: String(draft.company).trim(),
      height: ht,
      weight: wt,
      maritalStatus: draft.maritalStatus,
      contact: digits,
      kakao: String(draft.kakao).trim(),
      insta: String(draft.insta || '').trim(),
      region: String(draft.region).trim(),
      workplace: String(draft.workplace).trim(),
      smoke: draft.smoke,
      school: draft.school,
      salary: String(draft.salary).trim(),
      car: draft.car,
      marry: draft.marry,
      mbti: String(draft.mbti).trim().toUpperCase(),
    };

    setSaving(true);
    try {
      await onPersist(payload);
    } catch (e) {
      setErr(e.message || '저장에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const sel = (
    label,
    el,
    key,
  ) => (
    <div className="info-row" key={key}>
      <div className="k">{label}</div>
      <div className="v" style={{ overflow: 'visible' }}>{el}</div>
    </div>
  );

  return (
    <>
      <div style={{ padding: '18px 18px 14px', borderBottom: '1px solid var(--line)' }}>
        <div className="tiny">기본 프로필 · 신청 시 입력</div>
        <div className="tiny" style={{
          marginTop: 8, opacity: 0.75, lineHeight: 1.55, fontSize: 11.5,
        }}>
          수정 후 <b>기본 프로필 저장</b>을 누르면 서버에 반영되며 회원 목록·매칭 화면에도 동일 데이터가 적용됩니다.
        </div>
      </div>
      <div className="info-table" style={{ border: 0, borderRadius: 0 }}>
        {sel('이름', (
          <input className="input" value={draft.name} onChange={(e) => set({ name: e.target.value })} />
        ), 'name')}
        {sel('나이', (
          <input
            className="input"
            type="number"
            min={18}
            max={85}
            placeholder="예: 29"
            value={draft.age === '' ? '' : draft.age}
            onChange={(e) => set({ age: e.target.value.replace(/[^\d]/g, '') })}
          />
        ), 'age')}
        {sel('성별', (
          <select
            className="input"
            value={draft.gender}
            onChange={(e) => set({ gender: e.target.value })}
          >
            <option value="M">남자</option>
            <option value="F">여자</option>
          </select>
        ), 'gender')}
        {sel('직무', (
          <input className="input" value={draft.role} onChange={(e) => set({ role: e.target.value })} />
        ), 'role')}
        {sel('회사', (
          <input className="input" value={draft.company} onChange={(e) => set({ company: e.target.value })} />
        ), 'company')}
        {sel('키 (cm)', (
          <input
            className="input"
            type="number"
            min={120}
            max={230}
            value={draft.height === '' ? '' : draft.height}
            onChange={(e) => set({ height: e.target.value })}
          />
        ), 'height')}
        {sel('몸무게 (kg)', (
          <input
            className="input"
            type="number"
            min={30}
            max={200}
            value={draft.weight === '' ? '' : draft.weight}
            onChange={(e) => set({ weight: e.target.value })}
          />
        ), 'weight')}
        {sel('돌싱 유무', (
          <select
            className="input"
            value={draft.maritalStatus}
            onChange={(e) => set({ maritalStatus: e.target.value })}
          >
            <option value="미혼">미혼</option>
            <option value="돌싱">돌싱</option>
          </select>
        ), 'marital')}
        {sel('연락처', (
          <input
            className="input"
            inputMode="numeric"
            value={draft.contact}
            placeholder="01012345678"
            onChange={(e) => set({ contact: e.target.value.replace(/\D/g, '').slice(0, 11) })}
          />
        ), 'contact')}
        {sel('카카오톡 ID', (
          <input className="input" value={draft.kakao} onChange={(e) => set({ kakao: e.target.value })} />
        ), 'kakao')}
        {sel('인스타그램 ID', (
          <input className="input" value={draft.insta} onChange={(e) => set({ insta: e.target.value })} />
        ), 'insta')}
        {sel('거주지', (
          <input className="input" value={draft.region} onChange={(e) => set({ region: e.target.value })} />
        ), 'region')}
        {sel('근무지', (
          <input className="input" value={draft.workplace} onChange={(e) => set({ workplace: e.target.value })} />
        ), 'workplace')}
        {sel('흡연 여부', (
          <select className="input" value={draft.smoke} onChange={(e) => set({ smoke: e.target.value })}>
            <option value="비흡연자">비흡연자</option>
            <option value="전자담배">전자담배</option>
            <option value="연초">연초</option>
          </select>
        ), 'smoke')}
        {sel('최종학력', (
          <select className="input" value={draft.school} onChange={(e) => set({ school: e.target.value })}>
            {schoolOpts.map((o) => (<option key={o} value={o}>{o}</option>))}
          </select>
        ), 'school')}
        {sel('연봉', (
          <input className="input" value={draft.salary} onChange={(e) => set({ salary: e.target.value })} />
        ), 'salary')}
        {sel('자차 여부', (
          <select className="input" value={draft.car} onChange={(e) => set({ car: e.target.value })}>
            <option value="있음">있음</option>
            <option value="없음">없음</option>
          </select>
        ), 'car')}
        {sel('결혼 희망', (
          <select className="input" value={draft.marry} onChange={(e) => set({ marry: e.target.value })}>
            <option value="O">O</option>
            <option value="X">X</option>
            <option value="상대에 따라">상대에 따라</option>
            <option value="모르겠음">모르겠음</option>
          </select>
        ), 'marry')}
        {sel('MBTI', (
          <input
            className="input"
            maxLength={8}
            value={draft.mbti}
            onChange={(e) => set({ mbti: e.target.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 8) })}
          />
        ), 'mbti')}
      </div>
      <div style={{ padding: '14px 18px', borderTop: '1px solid var(--line)', background: 'var(--bg-soft)' }}>
        {err && (
          <div style={{
            marginBottom: 10, padding: '8px 10px', borderRadius: 10,
            background: '#fdecea', color: '#a8281d', fontSize: 12, lineHeight: 1.45,
          }}>{err}</div>
        )}
        <button
          type="button"
          className="btn primary full"
          disabled={saving || !member?._id}
          onClick={save}
          style={{ height: 48, fontWeight: 700 }}
        >{saving ? '저장 중…' : '기본 프로필 저장'}</button>
      </div>
    </>
  );
}

/**
 * 회원 상세 화면 인라인 편집용 — 한마디 / 자기소개 / 이상형 카드.
 *   - 기본은 읽기 모드: 본문 + 우상단 「수정」 버튼
 *   - 「수정」 누르면 textarea + 저장/취소
 *   - onSave({ [field]: value }, label) 호출 시 부모가 서버 PUT
 */
function EditableTextCard({
  title,
  field,
  value,
  placeholder,
  helper,
  emptyText = '— (입력된 값이 없습니다)',
  multiline = true,
  maxLength,
  onSave,
  disabled = false,
  quoteWrap = false,
}) {
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(value || '');
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState(null);

  React.useEffect(() => {
    if (!editing) {
      setDraft(value || '');
      setErr(null);
    }
  }, [value, editing]);

  const startEdit = () => {
    setDraft(value || '');
    setErr(null);
    setEditing(true);
  };
  const cancel = () => {
    setEditing(false);
    setErr(null);
  };
  const save = async () => {
    if (saving) return;
    setSaving(true);
    setErr(null);
    try {
      await onSave({ [field]: String(draft || '').trim() }, title);
      setEditing(false);
    } catch (e) {
      setErr(e?.message || '저장 실패');
    } finally {
      setSaving(false);
    }
  };

  const hasValue = !!String(value || '').trim();
  const wrap = (txt) => (quoteWrap && txt ? `“${txt}”` : txt);

  return (
    <div className="adm-card" style={{ position: 'relative' }}>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 8, flexWrap: 'wrap',
      }}>
        <div className="tiny">{title}</div>
        {!editing ? (
          <button
            type="button"
            onClick={startEdit}
            disabled={disabled}
            title={`${title} 수정`}
            style={{
              fontSize: 11.5, fontWeight: 700,
              padding: '4px 11px', borderRadius: 999,
              border: '1px solid var(--line)', background: '#fff',
              color: 'var(--ink-700)', cursor: disabled ? 'not-allowed' : 'pointer',
              fontFamily: 'var(--f-sans)', opacity: disabled ? 0.4 : 1,
              display: 'inline-flex', alignItems: 'center', gap: 4,
            }}
          >
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
              <path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4 12.5-12.5z"
                stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
            수정
          </button>
        ) : (
          <div style={{ display: 'inline-flex', gap: 6 }}>
            <button
              type="button"
              onClick={cancel}
              disabled={saving}
              style={{
                fontSize: 11.5, fontWeight: 600,
                padding: '4px 11px', borderRadius: 999,
                border: '1px solid var(--line)', background: '#fff',
                color: 'var(--ink-700)', cursor: 'pointer',
                fontFamily: 'var(--f-sans)',
              }}
            >취소</button>
            <button
              type="button"
              onClick={save}
              disabled={saving}
              style={{
                fontSize: 11.5, fontWeight: 700,
                padding: '4px 13px', borderRadius: 999,
                border: 0, background: '#161412', color: '#fff',
                cursor: saving ? 'not-allowed' : 'pointer',
                fontFamily: 'var(--f-sans)',
              }}
            >{saving ? '저장 중…' : '저장'}</button>
          </div>
        )}
      </div>

      {!editing ? (
        <div style={{
          marginTop: 10, fontSize: 14, lineHeight: 1.7,
          color: hasValue ? 'var(--ink-900)' : 'var(--ink-400)',
          padding: 14, background: 'var(--bg-soft)', borderRadius: 12,
          whiteSpace: 'pre-wrap', minHeight: 36,
        }}>
          {hasValue ? wrap(value) : emptyText}
        </div>
      ) : (
        <div style={{ marginTop: 10 }}>
          {multiline ? (
            <textarea
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              placeholder={placeholder || ''}
              maxLength={maxLength}
              style={{
                width: '100%', boxSizing: 'border-box',
                padding: 12, borderRadius: 12,
                border: '1px solid var(--line)',
                fontSize: 14, lineHeight: 1.7,
                fontFamily: 'inherit', color: 'var(--ink-900)',
                resize: 'vertical', minHeight: 110,
                background: '#fff', outline: 'none',
              }}
            />
          ) : (
            <input
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              placeholder={placeholder || ''}
              maxLength={maxLength}
              style={{
                width: '100%', boxSizing: 'border-box',
                padding: '10px 12px', borderRadius: 10,
                border: '1px solid var(--line)',
                fontSize: 14, fontFamily: 'inherit',
                color: 'var(--ink-900)', background: '#fff', outline: 'none',
              }}
            />
          )}
          {maxLength != null && (
            <div style={{
              marginTop: 4, fontSize: 11, color: 'var(--ink-400)',
              fontFamily: 'var(--f-mono)', textAlign: 'right',
            }}>
              {String(draft || '').length} / {maxLength}자
            </div>
          )}
          {err && (
            <div style={{
              marginTop: 6, padding: '7px 10px', borderRadius: 8,
              background: '#fdecea', color: '#a8281d', fontSize: 12,
            }}>{err}</div>
          )}
        </div>
      )}

      {helper && !editing && (
        <div className="tiny" style={{
          marginTop: 8, textTransform: 'none', letterSpacing: 0, fontFamily: 'var(--f-sans)',
        }}>
          {helper}
        </div>
      )}
    </div>
  );
}

/**
 * 회원 상세 화면 인라인 편집 — 선호 조건 카드.
 *   prefMbti(쉼표 입력) · prefTier(체크박스) · prefRegions(체크박스) · prefRegionOpen(토글)
 *   · prefAge[min,max] + prefAgeOpen(토글)
 */
function EditablePrefsCard({ member, onSave, disabled = false }) {
  const tierKeys = React.useMemo(() => getTierOrder(), []);
  const sidoList = React.useMemo(() => {
    return Array.isArray(window?.SIDO_LIST) && window.SIDO_LIST.length > 0
      ? window.SIDO_LIST
      : ['서울', '경기', '인천'];
  }, []);

  const initial = React.useCallback(() => ({
    prefMbti: Array.isArray(member?.prefMbti) ? member.prefMbti.join(', ') : '',
    prefTier: Array.isArray(member?.prefTier) ? [...member.prefTier] : [],
    prefRegions: Array.isArray(member?.prefRegions)
      ? [...member.prefRegions]
      : member?.prefRegion ? [member.prefRegion] : [],
    prefRegionOpen: !!member?.prefRegionOpen,
    prefAgeMin: Array.isArray(member?.prefAge) ? Number(member.prefAge[0] ?? 26) : 26,
    prefAgeMax: Array.isArray(member?.prefAge) ? Number(member.prefAge[1] ?? 32) : 32,
    prefAgeOpen: !!member?.prefAgeOpen,
  }), [member]);

  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(initial);
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState(null);

  React.useEffect(() => {
    if (!editing) setDraft(initial());
  }, [member, editing, initial]);

  const set = (patch) => setDraft((d) => ({ ...d, ...patch }));
  const toggleTier = (t) => setDraft((d) => ({
    ...d,
    prefTier: d.prefTier.includes(t)
      ? d.prefTier.filter((x) => x !== t)
      : [...d.prefTier, t],
  }));
  const toggleRegion = (r) => setDraft((d) => ({
    ...d,
    prefRegions: d.prefRegions.includes(r)
      ? d.prefRegions.filter((x) => x !== r)
      : [...d.prefRegions, r],
  }));

  const startEdit = () => { setDraft(initial()); setErr(null); setEditing(true); };
  const cancel = () => { setEditing(false); setErr(null); };

  const save = async () => {
    if (saving) return;
    setErr(null);

    const mbti = String(draft.prefMbti || '')
      .split(/[,\s]+/)
      .map((s) => s.trim().toUpperCase())
      .filter(Boolean);

    const lo = parseInt(String(draft.prefAgeMin), 10);
    const hi = parseInt(String(draft.prefAgeMax), 10);
    if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
      setErr('선호 나이는 숫자로 입력해주세요.');
      return;
    }
    if (lo < 18 || hi > 85) {
      setErr('선호 나이는 18–85세 범위입니다.');
      return;
    }
    if (lo > hi) {
      setErr('선호 나이 최소값이 최대값보다 큽니다.');
      return;
    }

    const payload = {
      prefMbti: mbti,
      prefTier: draft.prefTier,
      prefRegions: draft.prefRegions,
      prefRegionOpen: !!draft.prefRegionOpen,
      prefAge: [lo, hi],
      prefAgeOpen: !!draft.prefAgeOpen,
    };

    setSaving(true);
    try {
      await onSave(payload, '선호 조건');
      setEditing(false);
    } catch (e) {
      setErr(e?.message || '저장 실패');
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="adm-card">
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 8, flexWrap: 'wrap',
      }}>
        <div className="tiny">선호 조건</div>
        {!editing ? (
          <button
            type="button"
            onClick={startEdit}
            disabled={disabled}
            style={{
              fontSize: 11.5, fontWeight: 700,
              padding: '4px 11px', borderRadius: 999,
              border: '1px solid var(--line)', background: '#fff',
              color: 'var(--ink-700)', cursor: disabled ? 'not-allowed' : 'pointer',
              fontFamily: 'var(--f-sans)', opacity: disabled ? 0.4 : 1,
              display: 'inline-flex', alignItems: 'center', gap: 4,
            }}
          >
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
              <path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4 12.5-12.5z"
                stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
            수정
          </button>
        ) : (
          <div style={{ display: 'inline-flex', gap: 6 }}>
            <button
              type="button"
              onClick={cancel}
              disabled={saving}
              style={{
                fontSize: 11.5, fontWeight: 600,
                padding: '4px 11px', borderRadius: 999,
                border: '1px solid var(--line)', background: '#fff',
                color: 'var(--ink-700)', cursor: 'pointer',
                fontFamily: 'var(--f-sans)',
              }}
            >취소</button>
            <button
              type="button"
              onClick={save}
              disabled={saving}
              style={{
                fontSize: 11.5, fontWeight: 700,
                padding: '4px 13px', borderRadius: 999,
                border: 0, background: '#161412', color: '#fff',
                cursor: saving ? 'not-allowed' : 'pointer',
                fontFamily: 'var(--f-sans)',
              }}
            >{saving ? '저장 중…' : '저장'}</button>
          </div>
        )}
      </div>

      {!editing ? (
        // ── 읽기 모드 (기존 UI 유지)
        <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <PrefLine k="선호 MBTI" v={
            (!member.prefMbti || member.prefMbti.length === 0)
              ? '상관없음'
              : member.prefMbti.join(', ')
          } mono />
          <PrefLine k="선호 등급">
            <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
              {(member.prefTier || []).map((pt) => <TierChip key={pt} tier={pt} />)}
              {(!member.prefTier || member.prefTier.length === 0) && (
                <span style={{ color: 'var(--ink-500)' }}>상관없음</span>
              )}
            </div>
          </PrefLine>
          <PrefLine k="선호 지역" v={
            member.prefRegionOpen
              ? '상관없음'
              : ((member.prefRegions && member.prefRegions.length > 0)
                  ? member.prefRegions.join(', ')
                  : (member.prefRegion || '—'))
          } />
          <PrefLine k="선호 나이" v={member.prefAgeOpen ? '상관없음' : `${(member.prefAge || [])[0]} — ${(member.prefAge || [])[1]}살`} mono />
        </div>
      ) : (
        // ── 편집 모드
        <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 14 }}>
          {/* MBTI */}
          <div>
            <label style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-700)', display: 'block', marginBottom: 6 }}>
              선호 MBTI
              <span style={{ marginLeft: 6, fontWeight: 500, color: 'var(--ink-400)' }}>
                · 쉼표로 구분 · 비워두면 상관없음
              </span>
            </label>
            <input
              value={draft.prefMbti}
              onChange={(e) => set({ prefMbti: e.target.value.toUpperCase().replace(/[^A-Z, ]/g, '') })}
              placeholder="예: INFJ, ENFP, ENFJ"
              style={{
                width: '100%', boxSizing: 'border-box',
                padding: '10px 12px', borderRadius: 10,
                border: '1px solid var(--line)',
                fontSize: 13, fontFamily: 'var(--f-mono)',
                background: '#fff', outline: 'none',
              }}
            />
          </div>

          {/* 등급 */}
          <div>
            <label style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-700)', display: 'block', marginBottom: 8 }}>
              선호 등급
              <span style={{ marginLeft: 6, fontWeight: 500, color: 'var(--ink-400)' }}>
                · 비워두면 상관없음
              </span>
            </label>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {tierKeys.map((t) => {
                const checked = draft.prefTier.includes(t);
                return (
                  <button
                    key={t}
                    type="button"
                    onClick={() => toggleTier(t)}
                    style={{
                      padding: '6px 12px', borderRadius: 999,
                      border: '1.5px solid ' + (checked ? '#161412' : 'var(--line)'),
                      background: checked ? '#161412' : '#fff',
                      color: checked ? '#fff' : 'var(--ink-700)',
                      fontFamily: 'var(--f-mono)', fontSize: 11, fontWeight: 700,
                      letterSpacing: '0.04em', cursor: 'pointer',
                    }}
                  >
                    {TIER_LABEL[t] || t}
                  </button>
                );
              })}
            </div>
          </div>

          {/* 지역 */}
          <div>
            <label style={{
              fontSize: 12, fontWeight: 700, color: 'var(--ink-700)',
              display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8,
            }}>
              <span>선호 지역</span>
              <label style={{
                display: 'inline-flex', alignItems: 'center', gap: 6,
                fontSize: 11, fontWeight: 600, color: 'var(--ink-700)',
                cursor: 'pointer',
              }}>
                <input
                  type="checkbox"
                  checked={!!draft.prefRegionOpen}
                  onChange={(e) => set({ prefRegionOpen: e.target.checked })}
                />
                상관없음
              </label>
            </label>
            <div style={{
              display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))', gap: 6,
              opacity: draft.prefRegionOpen ? 0.4 : 1,
              pointerEvents: draft.prefRegionOpen ? 'none' : 'auto',
            }}>
              {sidoList.map((r) => {
                const checked = draft.prefRegions.includes(r);
                return (
                  <button
                    key={r}
                    type="button"
                    onClick={() => toggleRegion(r)}
                    style={{
                      padding: '7px 4px', borderRadius: 8,
                      border: '1.5px solid ' + (checked ? '#161412' : 'var(--line)'),
                      background: checked ? '#161412' : '#fff',
                      color: checked ? '#fff' : 'var(--ink-700)',
                      fontSize: 12, fontWeight: 600,
                      cursor: 'pointer', fontFamily: 'inherit',
                    }}
                  >
                    {r}
                  </button>
                );
              })}
            </div>
          </div>

          {/* 나이 */}
          <div>
            <label style={{
              fontSize: 12, fontWeight: 700, color: 'var(--ink-700)',
              display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8,
            }}>
              <span>선호 나이</span>
              <label style={{
                display: 'inline-flex', alignItems: 'center', gap: 6,
                fontSize: 11, fontWeight: 600, color: 'var(--ink-700)',
                cursor: 'pointer',
              }}>
                <input
                  type="checkbox"
                  checked={!!draft.prefAgeOpen}
                  onChange={(e) => set({ prefAgeOpen: e.target.checked })}
                />
                상관없음
              </label>
            </label>
            <div style={{
              display: 'flex', alignItems: 'center', gap: 8,
              opacity: draft.prefAgeOpen ? 0.4 : 1,
              pointerEvents: draft.prefAgeOpen ? 'none' : 'auto',
            }}>
              <input
                type="number"
                min={18}
                max={85}
                value={draft.prefAgeMin}
                onChange={(e) => set({ prefAgeMin: e.target.value.replace(/[^\d]/g, '') })}
                style={{
                  width: 80, boxSizing: 'border-box',
                  padding: '9px 10px', borderRadius: 10,
                  border: '1px solid var(--line)',
                  fontSize: 13, fontFamily: 'var(--f-mono)',
                  textAlign: 'center', background: '#fff', outline: 'none',
                }}
              />
              <span style={{ color: 'var(--ink-500)', fontSize: 13 }}>—</span>
              <input
                type="number"
                min={18}
                max={85}
                value={draft.prefAgeMax}
                onChange={(e) => set({ prefAgeMax: e.target.value.replace(/[^\d]/g, '') })}
                style={{
                  width: 80, boxSizing: 'border-box',
                  padding: '9px 10px', borderRadius: 10,
                  border: '1px solid var(--line)',
                  fontSize: 13, fontFamily: 'var(--f-mono)',
                  textAlign: 'center', background: '#fff', outline: 'none',
                }}
              />
              <span style={{ color: 'var(--ink-500)', fontSize: 12 }}>살</span>
            </div>
          </div>

          {err && (
            <div style={{
              padding: '8px 10px', borderRadius: 8,
              background: '#fdecea', color: '#a8281d', fontSize: 12, lineHeight: 1.45,
            }}>{err}</div>
          )}
        </div>
      )}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
// 회원 상세
// ═══════════════════════════════════════════════════════════
function AdminDetail({ go, member, setTier, members, startMatch, seenNewMemberIds = [], onPurge, persistMemberProfile, persistMemberTextPrefs }) {
  if (!member) return null;
  const isNew = member.daysAgo <= 5;
  const [photoLightboxAt, setPhotoLightboxAt] = React.useState(-1);
  const detailPhotos = (member.photos || []).filter((p) => p && p.url);

  const photoNamePreview = () => {
    const name = sanitizePhotoFilenamePart(member.name) || '회원';
    const age = member.age != null ? String(member.age) : '';
    const code = sanitizePhotoFilenamePart(member.memberCode || member.id || '');
    return [name, age, code].filter(Boolean).join(' - ');
  };

  return (
    <AdminShell go={go} active="detail" members={members} seenNewMemberIds={seenNewMemberIds}>
      {/* Page head */}
      <div className="page-head">
        <div>
          <div className="crumb">
            <button onClick={() => go('admin-list')} style={{ color: 'var(--ink-500)' }}>회원 목록</button>
            <span>›</span>
            <span style={{ color: 'var(--ink-700)' }}>{member.id}</span>
            {isNew && <span className="new-badge" style={{ marginLeft: 8 }}>NEW</span>}
          </div>
          <h1>{member.name} <span style={{ fontSize: 18, color: 'var(--ink-500)', fontWeight: 500 }}>{member.age}, {member.gender === 'M' ? '남' : '여'}</span></h1>
        </div>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <button
            type="button"
            className="btn ghost sm"
            onClick={() => {
              const el = document.getElementById('cs-admin-memo-textarea');
              el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
              setTimeout(() => el?.focus(), 350);
            }}
          >메모 추가</button>
          <button
            type="button"
            className="btn ghost sm"
            onClick={() => onPurge && onPurge(member.id)}
            style={{ color: 'var(--brand-ink)' }}
          >파기 처리</button>
          <button className="btn primary sm" onClick={() => startMatch(member.id)}>
            매칭 진행 →
          </button>
        </div>
      </div>

      {/* Hero banner */}
      <div className="adm-card dark" style={{ marginBottom: 16, position: 'relative', overflow: 'hidden' }}>
        <div style={{
          position: 'absolute', top: -60, right: -60, width: 280, height: 280, borderRadius: 999,
          background: {
            black: 'rgba(255,255,255,0.05)', red: 'rgba(226,58,46,0.3)',
            blue: 'rgba(47,108,246,0.3)', yellow: 'rgba(245,196,25,0.3)',
            white: 'rgba(245,239,226,0.4)',
          }[member.tier],
          filter: 'blur(60px)',
        }} />
        <div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 24, flexWrap: 'wrap' }}>
          <div className="av" style={{
            width: 88, height: 88, borderRadius: 20, background: tierAvatarBg(member.tier),
            color: tierAvatarColor(member.tier),
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 34, fontWeight: 700,
            border: member.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
          }}>{member.name.charAt(0)}</div>
          <div style={{ flex: 1, minWidth: 200 }}>
            <TierChip tier={member.tier} />
            <div style={{ marginTop: 10, fontSize: 30, fontWeight: 700, letterSpacing: '-0.02em' }}>
              {member.name} · {member.job}
            </div>
            <div style={{ marginTop: 4, color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>
              {member.region} · 신청한 지 {member.daysAgo}일 · {member.id}
            </div>
          </div>
          <div style={{ display: 'flex', gap: 20 }}>
            <HeroStat label="키·몸무게 (cm/kg)" value={`${member.height} / ${member.weight}`} />
            <HeroStat label="MBTI" value={member.mbti} mono />
            <HeroStat label="연봉" value={member.salary} mono />
          </div>
        </div>
      </div>

      {/* Tier picker */}
      <div className="adm-card" style={{ marginBottom: 16 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 10 }}>
          <div>
            <div className="tiny">관리자 전용 · 색상 등급</div>
            <div className="chart-title" style={{ marginTop: 4 }}>현재 등급: <TierChip tier={member.tier} /></div>
          </div>
          <span style={{
            fontSize: 11, fontFamily: 'var(--f-mono)', color: 'var(--brand-ink)',
            background: 'var(--brand-soft)', padding: '5px 10px', borderRadius: 6,
          }}>회원에게 비노출</span>
        </div>
        <div className="tier-picker-grid">
          {getTierOrder().map((t) => {
            const active = member.tier === t;
            const color = TIER_COLORS[t];
            const lightTier = (t === 'yellow' || t === 'white' || t === 'diamond');
            return (
              <button
                key={t}
                className={`tier-pick ${active ? 'on' : ''}`}
                onClick={() => setTier(member.id, t)}
                style={active ? {
                  background: color,
                  borderColor: t === 'diamond' ? '#c8b8ff' : color,
                  color: lightTier ? '#161412' : '#fff',
                } : {}}
              >
                <div className="sw" style={{
                  background: color,
                  border: t === 'white'
                    ? '1px solid var(--ink-300)'
                    : (active ? '1.5px solid rgba(255,255,255,0.4)' : '1.5px solid rgba(0,0,0,0.08)'),
                  boxShadow: t === 'diamond' ? 'inset 0 0 6px rgba(255,255,255,0.6)' : 'none',
                }} />
                <div className="nm">{TIER_LABEL[t]}</div>
                <div style={{ fontSize: 10, fontFamily: 'var(--f-mono)', opacity: 0.6 }}>
                  순위 {TIER_RANK[t]}
                </div>
              </button>
            );
          })}
        </div>
        <div className="tiny" style={{ marginTop: 14, textAlign: 'center', textTransform: 'none', letterSpacing: 0 }}>
          다이아몬드(6) › 블랙(5) › 레드(4) › 블루(3) › 옐로(2) › 화이트(1)
        </div>
      </div>

      {/* Main detail grid */}
      <div className="detail-grid">
        {/* Left: photos + ideal */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <div className="adm-card">
            <div style={{
              display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
              gap: 10, flexWrap: 'wrap',
            }}>
              <div>
                <div className="tiny">제출 사진 (최대 5장)</div>
                <div className="tiny" style={{ marginTop: 6, opacity: 0.72, fontFamily: 'var(--f-mono)', fontSize: 10 }}>
                  파일명: {photoNamePreview() || '이름 - 나이 - 회원번호'} - 1 …
                </div>
              </div>
              <MemberProfilePhotoDownloadButtons member={member} />
            </div>
            <div className="detail-photos" style={{ marginTop: 12 }}>
              {Array.from({ length: 5 }).map((_, i) => {
                const photo = (member.photos || [])[i];
                const photoIndex = photo ? detailPhotos.findIndex(p => p === photo) : -1;
                const clickable = photoIndex >= 0;
                const style = photo
                  ? { backgroundImage: `url(${photo.url})`, backgroundSize: 'cover', backgroundPosition: 'center', cursor: 'zoom-in' }
                  : { background: `linear-gradient(135deg, hsl(${(i * 40 + 20) % 360}, 12%, 78%), hsl(${(i * 40 + 60) % 360}, 14%, 64%))` };
                const label = photo ? (photo.filename || `0${i + 1}.JPG`) : `0${i + 1}.JPG`;
                return (
                  <div
                    key={i}
                    className={`detail-photo ${i === 0 ? 'main' : ''}`}
                    style={style}
                    onClick={clickable ? () => setPhotoLightboxAt(photoIndex) : undefined}
                    role={clickable ? 'button' : undefined}
                    tabIndex={clickable ? 0 : undefined}
                    onKeyDown={clickable ? (e) => {
                      if (e.key === 'Enter' || e.key === ' ') setPhotoLightboxAt(photoIndex);
                    } : undefined}
                  >
                    <div className="pn">{label}</div>
                  </div>
                );
              })}
            </div>
            {photoLightboxAt >= 0 && detailPhotos.length > 0 && (
              <PhotoLightbox
                photos={detailPhotos}
                startIndex={photoLightboxAt}
                onClose={() => setPhotoLightboxAt(-1)}
              />
            )}
          </div>

          <EditableTextCard
            title="소개팅에 남긴 한마디"
            field="message"
            value={member.message}
            placeholder="회원이 신청서에 작성한 한마디"
            helper="💡 봇 필터링 + 진정성 확인용 (회원에게 비노출)"
            maxLength={1000}
            disabled={!persistMemberTextPrefs}
            onSave={async (payload, label) => {
              if (!persistMemberTextPrefs) throw new Error('저장 기능이 연결되지 않았습니다.');
              await persistMemberTextPrefs(member._id, payload, label);
            }}
          />

          <EditableTextCard
            title="자기소개"
            field="intro"
            value={member.intro}
            placeholder="회원이 본인을 소개한 글"
            maxLength={3000}
            disabled={!persistMemberTextPrefs}
            onSave={async (payload, label) => {
              if (!persistMemberTextPrefs) throw new Error('저장 기능이 연결되지 않았습니다.');
              await persistMemberTextPrefs(member._id, payload, label);
            }}
          />

          <EditableTextCard
            title="이상형 (주관식)"
            field="ideal"
            value={member.ideal}
            placeholder="이상형을 자유롭게 서술"
            maxLength={2000}
            quoteWrap
            disabled={!persistMemberTextPrefs}
            onSave={async (payload, label) => {
              if (!persistMemberTextPrefs) throw new Error('저장 기능이 연결되지 않았습니다.');
              await persistMemberTextPrefs(member._id, payload, label);
            }}
          />

          <EditablePrefsCard
            member={member}
            disabled={!persistMemberTextPrefs}
            onSave={async (payload, label) => {
              if (!persistMemberTextPrefs) throw new Error('저장 기능이 연결되지 않았습니다.');
              await persistMemberTextPrefs(member._id, payload, label);
            }}
          />
        </div>

        {/* Right: 편집 가능한 기본 프로필 */}
        <div>
          <div className="adm-card" style={{ padding: 0, overflow: 'hidden' }}>
            <MemberBasicProfileEditor
              member={member}
              onPersist={persistMemberProfile
                ? (body) => persistMemberProfile(member._id, body)
                : async () => {
                  throw new Error('프로필 저장 기능이 연결되지 않았습니다.');
                }}
            />
          </div>

          <OperatorNote member={member} />
        </div>
      </div>
    </AdminShell>
  );
}

function OperatorNote({ member }) {
  const [memo, setMemo] = React.useState(member.adminMemo || '');
  const [saving, setSaving] = React.useState(false);
  const [msg, setMsg] = React.useState(null);

  React.useEffect(() => { setMemo(member.adminMemo || ''); }, [member._id]);

  const save = async () => {
    setSaving(true); setMsg(null);
    try {
      await window.CSApi.updateMemo(member._id, memo);
      setMsg('저장됨');
      setTimeout(() => setMsg(null), 1800);
    } catch (err) {
      setMsg(err.message || '저장 실패');
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="adm-card" style={{ marginTop: 16, background: '#161412', color: '#fff', border: 0 }}>
      <div className="tiny" style={{ color: 'rgba(255,255,255,0.5)' }}>내부 메모</div>
      <textarea
        id="cs-admin-memo-textarea"
        value={memo}
        onChange={e => setMemo(e.target.value)}
        placeholder="내부 메모 (회원에게 비공개)"
        style={{
          width: '100%', marginTop: 10, background: 'transparent',
          border: '1px solid rgba(255,255,255,0.12)', borderRadius: 10,
          padding: 12, color: '#fff', fontSize: 13, resize: 'vertical',
          minHeight: 80,
        }}
      />
      <div style={{
        marginTop: 10, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        fontSize: 11, fontFamily: 'var(--f-mono)', color: 'rgba(255,255,255,0.4)',
      }}>
        <span>{msg || '저장하면 서버에 반영됩니다'}</span>
        <button onClick={save} disabled={saving} style={{
          background: '#fff', color: '#161412',
          padding: '6px 14px', borderRadius: 999, fontSize: 11,
          fontFamily: 'var(--f-mono)', letterSpacing: '0.06em', textTransform: 'uppercase',
          opacity: saving ? 0.6 : 1, cursor: saving ? 'not-allowed' : 'pointer',
        }}>{saving ? '저장중' : '저장'}</button>
      </div>
    </div>
  );
}

function HeroStat({ label, value, mono }) {
  return (
    <div>
      <div style={{ fontSize: 10, color: 'rgba(255,255,255,0.5)', fontFamily: 'var(--f-mono)', letterSpacing: '0.08em' }}>
        {label}
      </div>
      <div style={{ marginTop: 6, fontSize: 17, fontWeight: 600, fontFamily: mono ? 'var(--f-mono)' : 'var(--f-sans)' }}>
        {value}
      </div>
    </div>
  );
}

function PrefLine({ k, v, mono, children }) {
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, fontSize: 13.5 }}>
      <div style={{ width: 80, color: 'var(--ink-500)', fontSize: 12, flexShrink: 0 }}>{k}</div>
      <div style={{ flex: 1, color: 'var(--ink-900)', fontFamily: mono ? 'var(--f-mono)' : 'var(--f-sans)' }}>
        {children || v}
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// 매칭 진행 (선택된 회원과 반대 성별 후보 매칭)
// ═════════════════════════════════════════════════════════════

/** 선호 지역이 비어 있거나 무관이면 지역 조건 없음으로 간주 */
function regionPrefOpen(m) {
  if (!m) return true;
  if (m.prefRegionOpen) return true;
  const pr = (m.prefRegions || []).filter(Boolean);
  if (pr.length) return false;
  return !String(m.prefRegion || '').trim();
}

function formatRegionPrefs(m) {
  if (regionPrefOpen(m)) return '무관';
  const pr = (m.prefRegions || []).filter(Boolean);
  if (pr.length) return pr.join(', ');
  return String(m.prefRegion || '').trim() || '무관';
}

/** picker가 지역을 가리킬 때, target 거주지가 그 선호에 맞는지 */
function regionMatchesPersonPrefs(picker, target) {
  const prefs = (picker.prefRegions || []).filter(Boolean);
  const legacy = String(picker.prefRegion || '').trim();
  const r = String(target.region || '');
  if (!r) return false;
  if (prefs.length) return prefs.some((p) => r.includes(p));
  if (legacy) return r.includes(legacy);
  return true;
}

function formatTierPrefs(m) {
  const arr = m.prefTier || [];
  if (!arr.length) return '무관';
  return arr.map((t) => TIER_LABEL[t] || t).join(', ');
}

function formatMbtiPrefs(m) {
  const arr = m.prefMbti || [];
  if (!arr.length) return '무관';
  return arr.join(', ');
}

// Compatibility scoring (총점 100: 등급30·나이25·MBTI8·지역27·흡연10)
function computeScore(source, candidate) {
  const breakdown = [];
  let score = 0;

  const T_MAX = 30;
  const A_MAX = 25;
  const M_MAX = 8;
  const R_MAX = 27;
  const S_MAX = 10;

  // ── 등급 (양쪽 선호 유무 반영 + 어느 쪽이 안 맞는지 문구) ──
  const srcWantsTier = (source.prefTier || []).length > 0;
  const candWantsTier = (candidate.prefTier || []).length > 0;
  const srcTierOk = !srcWantsTier || (source.prefTier || []).includes(candidate.tier);
  const candTierOk = !candWantsTier || (candidate.prefTier || []).includes(source.tier);

  let tPts = 0;
  let tNote = '';
  if (!srcWantsTier && !candWantsTier) {
    tPts = T_MAX;
    tNote = '양쪽 모두 등급 선호 없음 → 무관 처리';
  } else if (srcTierOk && candTierOk) {
    tPts = T_MAX;
    tNote = '서로의 희망 등급에 상대 등급이 포함됨';
  } else if (srcTierOk && !candTierOk) {
    tPts = 15;
    tNote = `후보(${candidate.name}) 희망 등급: ${formatTierPrefs(candidate)}. 본인 등급(${TIER_LABEL[source.tier]})은 후보 선호에 없음.`;
  } else if (!srcTierOk && candTierOk) {
    tPts = 15;
    tNote = `본인(${source.name}) 희망 등급: ${formatTierPrefs(source)}. 후보 등급(${TIER_LABEL[candidate.tier]})은 본인 선호에 없음.`;
  } else {
    tNote = `본인 희망: ${formatTierPrefs(source)} · 후보 희망: ${formatTierPrefs(candidate)} → 양쪽 조건 모두 불일치`;
  }
  score += tPts;
  breakdown.push({ k: '등급', s: tPts, max: T_MAX, note: tNote });

  // ── 나이 ──
  const ageOk =
    source.prefAgeOpen ||
    (candidate.age >= source.prefAge[0] && candidate.age <= source.prefAge[1]);
  const ageRev =
    candidate.prefAgeOpen ||
    (source.age >= candidate.prefAge[0] && source.age <= candidate.prefAge[1]);
  let aPts = 0;
  let aNote = '';
  if (ageOk && ageRev) {
    aPts = A_MAX;
    aNote = `본인 희망 나이 ${source.prefAgeOpen ? '무관' : `${source.prefAge[0]}–${source.prefAge[1]}살`} · 후보 ${candidate.age}세 / 후보 희망 ${candidate.prefAgeOpen ? '무관' : `${candidate.prefAge[0]}–${candidate.prefAge[1]}살`} · 본인 ${source.age}세 → 쌍방 범위 충족`;
  } else if (ageOk && !ageRev) {
    aPts = 12;
    aNote = `본인 기준으로 후보 나이(${candidate.age}세)는 OK. 후보가 원하는 나이에는 본인(${source.age}세)이 맞지 않음.`;
  } else if (!ageOk && ageRev) {
    aPts = 12;
    aNote = `후보 기준으로 본인 나이(${source.age}세)는 OK. 본인이 원하는 나이에는 후보(${candidate.age}세)가 맞지 않음.`;
  } else {
    aNote = `본인 희망 ${source.prefAgeOpen ? '무관' : `${source.prefAge[0]}–${source.prefAge[1]}살`}, 후보 희망 ${candidate.prefAgeOpen ? '무관' : `${candidate.prefAge[0]}–${candidate.prefAge[1]}살`} → 양쪽 범위 모두 어긋남`;
  }
  score += aPts;
  breakdown.push({ k: '나이', s: aPts, max: A_MAX, note: aNote });

  // ── MBTI (비중 낮춤: 만점 8) ──
  const srcMbtiRelaxed = !source.prefMbti?.length || source.prefMbti.includes('상관없음');
  const candMbtiRelaxed = !candidate.prefMbti?.length || candidate.prefMbti.includes('상관없음');
  const srcMbtiOk = srcMbtiRelaxed || (source.prefMbti || []).includes(candidate.mbti);
  const candMbtiOk = candMbtiRelaxed || (candidate.prefMbti || []).includes(source.mbti);

  let mbPts = 0;
  let mbNote = '';
  if (srcMbtiRelaxed && candMbtiRelaxed) {
    mbPts = M_MAX;
    mbNote = '양쪽 MBTI 선호 없음 또는 무관';
  } else if (srcMbtiOk && candMbtiOk) {
    mbPts = M_MAX;
    mbNote = `서로 선호 MBTI에 상대 유형 포함 (본인 ${source.mbti} · 후보 ${candidate.mbti})`;
  } else if (srcMbtiOk || candMbtiOk) {
    mbPts = 4;
    if (!srcMbtiOk) {
      mbNote = `본인 선호 MBTI(${formatMbtiPrefs(source)})에 후보 유형 ${candidate.mbti} 없음. 후보→본인 조건은 충족.`;
    } else {
      mbNote = `후보 선호 MBTI(${formatMbtiPrefs(candidate)})에 본인 유형 ${source.mbti} 없음. 본인→후보 조건은 충족.`;
    }
  } else {
    mbPts = 1;
    mbNote = `본인 선호: ${formatMbtiPrefs(source)} · 후보 선호: ${formatMbtiPrefs(candidate)} → MBTI 조건 양쪽 미충족`;
  }
  score += mbPts;
  breakdown.push({ k: 'MBTI', s: mbPts, max: M_MAX, note: mbNote });

  // ── 지역 (선호 미입력·무관 = 감점 없음, 양방향 판정) ──
  const sOpen = regionPrefOpen(source);
  const cOpen = regionPrefOpen(candidate);
  const sOk = sOpen || regionMatchesPersonPrefs(source, candidate);
  const cOk = cOpen || regionMatchesPersonPrefs(candidate, source);

  let rPts = 0;
  let rNote = '';
  if (sOpen && cOpen) {
    rPts = R_MAX;
    rNote = `양쪽 모두 선호 지역 무관(미입력 포함). 실제 거주: 본인 ${source.region || '—'} · 후보 ${candidate.region || '—'}`;
  } else if (sOpen && !cOpen) {
    if (cOk) {
      rPts = R_MAX;
      rNote = `본인은 지역 무관. 후보 선호(${formatRegionPrefs(candidate)})에 본인 거주(${source.region || '—'}) 부합.`;
    } else {
      rPts = 13;
      rNote = `본인은 지역 무관. 후보 선호(${formatRegionPrefs(candidate)})에 본인 거주(${source.region || '—'}) 불일치 → 후보만 조건 적용.`;
    }
  } else if (!sOpen && cOpen) {
    if (sOk) {
      rPts = R_MAX;
      rNote = `후보는 지역 무관. 본인 선호(${formatRegionPrefs(source)})에 후보 거주(${candidate.region || '—'}) 부합.`;
    } else {
      rPts = 13;
      rNote = `후보는 지역 무관. 본인 선호(${formatRegionPrefs(source)})에 후보 거주(${candidate.region || '—'}) 불일치.`;
    }
  } else if (sOk && cOk) {
    rPts = R_MAX;
    rNote = `본인 선호(${formatRegionPrefs(source)})·후보 선호(${formatRegionPrefs(candidate)}) 모두에 상대 거주 부합.`;
  } else if (sOk || cOk) {
    rPts = 17;
    const parts = [];
    if (!sOk) parts.push(`본인 선호(${formatRegionPrefs(source)}) ← 후보 ${candidate.region || '—'}`);
    if (!cOk) parts.push(`후보 선호(${formatRegionPrefs(candidate)}) ← 본인 ${source.region || '—'}`);
    rNote = `한쪽만 부합 · ${parts.join(' / ')}`;
  } else {
    rPts = 6;
    rNote = `양쪽 선호 모두 불일치. 본인 선호: ${formatRegionPrefs(source)} (${source.region}) · 후보 선호: ${formatRegionPrefs(candidate)} (${candidate.region})`;
  }
  score += rPts;
  breakdown.push({ k: '지역', s: rPts, max: R_MAX, note: rNote });

  // ── 흡연 ──
  let smPts = 0;
  let smNote = '';
  if (source.smoke === candidate.smoke) {
    smPts = S_MAX;
    smNote = `흡연 성향 동일 (${source.smoke || '—'})`;
  } else if (source.smoke === '비흡연자' && candidate.smoke !== '비흡연자') {
    smNote = `본인 비흡연 · 후보 ${candidate.smoke || '흡연'} → 본인 기준 부담 가능`;
  } else {
    smPts = 5;
    smNote = `흡연 성향 다름(본인 ${source.smoke || '—'} · 후보 ${candidate.smoke || '—'}) → 중간 점수`;
  }
  score += smPts;
  breakdown.push({ k: '흡연', s: smPts, max: S_MAX, note: smNote });

  return { score, breakdown };
}

function AdminMatch({ go, source, members, onConfirm, onPropose, onSelectFinal, onRejectCandidate, matches, proposalGroups = [], seenNewMemberIds = [], openDetail }) {
  const [candidateId, setCandidateId] = React.useState(null);
  const [relayIds, setRelayIds] = React.useState(() => new Set());
  const [relayOpen, setRelayOpen] = React.useState(false);

  const toggleRelay = (id, e) => {
    if (e) { e.stopPropagation(); e.preventDefault(); }
    setRelayIds((s) => {
      const next = new Set(s);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };
  const [tierFilter, setTierFilter] = React.useState('all');
  const [search, setSearch] = React.useState('');
  const [confirmOpen, setConfirmOpen] = React.useState(false);

  if (!source) return null;
  const oppositeGender = source.gender === 'M' ? 'F' : 'M';
  const alreadyMatchedIds = new Set(
    (matches || []).filter(m => m.a === source.id || m.b === source.id).map(m => m.a === source.id ? m.b : m.a)
  );

  const candidates = React.useMemo(() => {
    const list = members
      .filter(m => m.gender === oppositeGender && m.id !== source.id)
      .map(m => ({ ...m, ...computeScore(source, m), matched: alreadyMatchedIds.has(m.id) }))
      .sort((a, b) => b.score - a.score);
    return list.filter(c => {
      if (tierFilter !== 'all' && c.tier !== tierFilter) return false;
      if (search) {
        const ql = search.toLowerCase();
        if (!c.name.toLowerCase().includes(ql) && !c.job.toLowerCase().includes(ql) && !c.region.toLowerCase().includes(ql) && !(c.workplace || '').toLowerCase().includes(ql)) return false;
      }
      return true;
    });
  }, [members, source, tierFilter, search]);

  const candidate = candidates.find(c => c.id === candidateId);

  const scoreColor = (s) => {
    if (s >= 75) return { bg: '#e23a2e', label: '매우 높음' };
    if (s >= 60) return { bg: '#f5c419', label: '높음' };
    if (s >= 40) return { bg: '#2f6cf6', label: '보통' };
    return { bg: '#94918a', label: '낮음' };
  };

  return (
    <AdminShell go={go} active="match" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head" style={{ alignItems: 'center' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14, minWidth: 0 }}>
          <button
            onClick={() => go('admin-detail')}
            aria-label="회원 상세로 돌아가기"
            title="뒤로 (회원 상세)"
            style={{
              width: 44, height: 44, borderRadius: 14, flexShrink: 0,
              background: '#fff', border: '1px solid var(--line)',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
              cursor: 'pointer', transition: 'background 120ms ease',
            }}
            onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }}
            onMouseLeave={e => { e.currentTarget.style.background = '#fff'; }}
          >
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
              <path d="M15 6l-6 6 6 6" stroke="#161412" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </button>
          <div style={{ minWidth: 0 }}>
            <div className="crumb">
              <button onClick={() => go('admin-detail')} style={{ color: 'var(--ink-500)' }}>회원 상세</button>
              <span>›</span>
              <span style={{ color: 'var(--ink-700)' }}>매칭 진행</span>
            </div>
            <h1>
              <MemberNameLink member={source} openDetail={openDetail} />
              <span style={{ fontSize: 18, color: 'var(--ink-500)', fontWeight: 500, marginLeft: 8 }}>님의 매칭 후보</span>
            </h1>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn ghost sm" onClick={() => go('admin-detail')}>← 상세로</button>
        </div>
      </div>

      <div className="match-grid">
        {/* ── Left: Source member ── */}
        <div className="match-col-side">
          <MatchMemberCard m={source} role="본인" openDetail={openDetail} />
        </div>

        {/* ── Center: Candidate list ── */}
        <div className="match-col-main">
          <div className="adm-card" style={{ padding: 14, marginBottom: 12 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
              <div className="tiny" style={{ flex: '0 0 100%' }}>
                {source.gender === 'M' ? '여자' : '남자'} 후보 · 호환도 순 정렬 · {candidates.length}명
              </div>
              <div style={{
                flex: 1, minWidth: 200, display: 'flex', alignItems: 'center', gap: 10,
                background: 'var(--bg-soft)', borderRadius: 12, padding: '10px 14px',
              }}>
                <span style={{ color: 'var(--ink-400)' }}>⌕</span>
                <input value={search} onChange={e => setSearch(e.target.value)} placeholder="이름·직업·지역 검색"
                  style={{ flex: 1, border: 0, background: 'transparent', fontSize: 13 }} />
              </div>
              <div style={{
                display: 'inline-flex', background: 'var(--bg-soft)', borderRadius: 10, padding: 3,
                flexWrap: 'wrap', gap: 2,
              }}>
                {[
                  { v: 'all', l: '전체' },
                  ...source.prefTier.map(t => ({ v: t, l: TIER_LABEL[t] })),
                ].map(t => (
                  <button
                    key={t.v}
                    onClick={() => setTierFilter(t.v)}
                    style={{
                      padding: '7px 12px', borderRadius: 8,
                      fontSize: 11.5, fontWeight: 600,
                      fontFamily: t.v === 'all' ? 'inherit' : 'var(--f-mono)',
                      letterSpacing: t.v === 'all' ? 0 : '0.04em',
                      background: tierFilter === t.v ? '#fff' : 'transparent',
                      color: tierFilter === t.v ? 'var(--ink-900)' : 'var(--ink-500)',
                      boxShadow: tierFilter === t.v ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
                    }}
                  >{t.l}</button>
                ))}
              </div>
            </div>
          </div>

          {/* 선택 액션 바 — 체크된 후보가 있으면 상단에 sticky 노출 */}
          <div
            style={{
              display: 'flex', alignItems: 'center', justifyContent: 'space-between',
              gap: 10, padding: '10px 14px', marginBottom: 8,
              background: relayIds.size > 0 ? '#161412' : 'var(--bg-soft)',
              color: relayIds.size > 0 ? '#fff' : 'var(--ink-500)',
              borderRadius: 12, transition: 'background 140ms ease, color 140ms ease',
              border: '1px solid ' + (relayIds.size > 0 ? '#161412' : 'var(--line)'),
            }}
          >
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13 }}>
              <span style={{
                fontFamily: 'var(--f-mono)', fontSize: 11, letterSpacing: '0.06em',
                opacity: 0.7, textTransform: 'uppercase',
              }}>선택</span>
              <b style={{ fontSize: 14, fontWeight: 700 }}>{relayIds.size}명</b>
              <span style={{ opacity: 0.65 }}>
                {relayIds.size > 0 ? '· 당사자에게 카톡으로 전달할 후보' : '· 후보 카드 좌측의 검정 체크박스를 눌러 선택'}
              </span>
            </div>
            <div style={{ display: 'flex', gap: 6 }}>
              {relayIds.size > 0 && (
                <button
                  onClick={() => setRelayIds(new Set())}
                  style={{
                    padding: '7px 12px', borderRadius: 999,
                    background: 'rgba(255,255,255,0.08)', color: '#fff',
                    fontSize: 12, fontWeight: 600,
                  }}
                >초기화</button>
              )}
              <button
                onClick={() => setRelayOpen(true)}
                disabled={relayIds.size === 0}
                style={{
                  padding: '8px 14px', borderRadius: 999,
                  background: relayIds.size > 0 ? '#fff' : 'transparent',
                  color: relayIds.size > 0 ? '#161412' : 'var(--ink-400)',
                  fontSize: 13, fontWeight: 700,
                  border: '1px solid ' + (relayIds.size > 0 ? '#fff' : 'var(--line)'),
                  cursor: relayIds.size > 0 ? 'pointer' : 'not-allowed',
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                }}
              >
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
                  <path d="M3 11l18-8-8 18-2-8-8-2z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
                </svg>
                당사자에게 리스트 전달
              </button>
            </div>
          </div>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {candidates.map(c => {
              const sc = scoreColor(c.score);
              const isActive = candidateId === c.id;
              const isChecked = relayIds.has(c.id);
              return (
                <div
                  key={c.id}
                  role="button"
                  tabIndex={0}
                  aria-pressed={isActive}
                  onClick={() => setCandidateId(c.id)}
                  onKeyDown={(e) => {
                    if (e.key === 'Enter' || e.key === ' ') {
                      e.preventDefault();
                      setCandidateId(c.id);
                    }
                  }}
                  style={{
                    width: '100%', textAlign: 'left',
                    display: 'flex', alignItems: 'center', gap: 14,
                    background: isChecked ? '#fff8f7' : '#fff',
                    border: `1.5px solid ${isChecked ? '#ec4438' : (isActive ? 'var(--ink-900)' : 'var(--line)')}`,
                    borderRadius: 16, padding: '12px 14px',
                    transition: 'all 120ms ease', position: 'relative',
                    opacity: c.matched ? 0.55 : 1,
                    cursor: 'pointer',
                    boxSizing: 'border-box',
                  }}
                >
                  {/* 릴레이 체크박스 */}
                  <span
                    role="checkbox"
                    aria-checked={isChecked}
                    aria-label={`${c.name} 선택`}
                    tabIndex={0}
                    onClick={(e) => toggleRelay(c.id, e)}
                    onKeyDown={(e) => {
                      if (e.key === ' ' || e.key === 'Enter') toggleRelay(c.id, e);
                    }}
                    style={{
                      flexShrink: 0,
                      width: 26, height: 26, borderRadius: 8,
                      border: `2px solid ${isChecked ? '#ec4438' : '#161412'}`,
                      background: isChecked ? '#ec4438' : '#fff',
                      boxShadow: isChecked
                        ? '0 2px 6px rgba(236,68,56,0.35)'
                        : 'inset 0 0 0 1px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06)',
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                      color: '#fff', fontSize: 14, fontWeight: 800, lineHeight: 1,
                      transition: 'all 120ms ease',
                      cursor: 'pointer',
                    }}
                  >{isChecked ? '✓' : ''}</span>

                  <MemberPhotoAvatar member={c} size={56} radius={14} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, flexWrap: 'wrap' }}>
                      <MemberNameLink member={c} openDetail={openDetail} stopPropagation style={{ fontSize: 15, fontWeight: 700, letterSpacing: '-0.01em' }} />
                      <span style={{ fontSize: 13, color: 'var(--ink-500)' }}>{c.age}</span>
                      <TierChip tier={c.tier} />
                      {(c.photos || []).length > 0 && (
                        <span style={{
                          fontFamily: 'var(--f-mono)', fontSize: 10, fontWeight: 600,
                          background: 'var(--bg-soft)', color: 'var(--ink-700)',
                          padding: '2px 7px', borderRadius: 999, letterSpacing: '0.04em',
                        }}>📷 {c.photos.length}</span>
                      )}
                      {c.matched && (
                        <span style={{
                          fontFamily: 'var(--f-mono)', fontSize: 10, fontWeight: 700,
                          background: '#161412', color: '#fff',
                          padding: '2px 8px', borderRadius: 999, letterSpacing: '0.04em',
                        }}>매칭됨</span>
                      )}
                    </div>
                    <div style={{ fontSize: 12.5, color: 'var(--ink-500)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {[
                        c.job,
                        formatMemberHeightCm(c),
                        c.region.split(' ').slice(0, 2).join(' '),
                        c.mbti,
                      ].filter(Boolean).join(' · ')}
                    </div>
                    <CompatibilityBar breakdown={c.breakdown} />
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
                    <MemberProfilePhotoDownloadButtons member={c} compact />
                    <div style={{ width: 70, flexShrink: 0, textAlign: 'right' }}>
                      <div style={{
                        display: 'inline-flex', alignItems: 'baseline', gap: 3,
                        background: sc.bg, color: sc.bg === '#f5c419' ? '#161412' : '#fff',
                        padding: '4px 10px', borderRadius: 999,
                        fontFamily: 'var(--f-mono)', fontSize: 14, fontWeight: 700,
                      }}>{c.score}<span style={{ fontSize: 9, fontWeight: 500, opacity: 0.8 }}>/100</span></div>
                      <div style={{ fontFamily: 'var(--f-mono)', fontSize: 9, color: 'var(--ink-400)', marginTop: 4, letterSpacing: '0.06em' }}>
                        {sc.label}
                      </div>
                    </div>
                  </div>
                </div>
              );
            })}
            {candidates.length === 0 && (
              <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-400)', fontSize: 14 }}>
                조건에 맞는 후보가 없어요.
              </div>
            )}
          </div>
        </div>

        {/* ── Right: Compat detail / action ── */}
        <div className="match-col-side">
          {candidate ? (
            <MatchCompatPanel
              source={source}
              candidate={candidate}
              onConfirm={() => setConfirmOpen(true)}
              isInRelay={relayIds.has(candidate.id)}
              onToggleRelay={() => toggleRelay(candidate.id)}
              openDetail={openDetail}
            />
          ) : (
            <div className="adm-card" style={{ padding: 28, textAlign: 'center', color: 'var(--ink-400)' }}>
              <div style={{ fontSize: 40, lineHeight: 1, marginBottom: 14 }}>👈</div>
              <div style={{ fontSize: 14, color: 'var(--ink-500)', lineHeight: 1.6 }}>
                후보를 선택하면 호환도 상세와<br />매칭 진행 버튼이 여기에 표시돼요.
              </div>
            </div>
          )}
        </div>
      </div>

      {/* ── 매칭 기록 트리 — 이 회원이 받은 제안 그룹들 ─────────── */}
      <MatchProposalsHistory
        source={source}
        proposalGroups={(proposalGroups || []).filter((g) => {
          const sid = String(g?.source?._id || '');
          return sid === String(source?._id || '');
        })}
        onSelectFinal={onSelectFinal}
        onReject={onRejectCandidate}
        members={members}
        openDetail={openDetail}
      />

      {confirmOpen && candidate && (
        <ConfirmMatchModal
          source={source}
          candidate={candidate}
          onCancel={() => setConfirmOpen(false)}
          onConfirm={() => {
            onConfirm(source.id, candidate.id);
            setConfirmOpen(false);
            setCandidateId(null);
          }}
          openDetail={openDetail}
        />
      )}

      {relayOpen && (
        <RelayCandidatesModal
          source={source}
          candidates={candidates.filter(c => relayIds.has(c.id))}
          onClose={() => setRelayOpen(false)}
          onRemove={(id) => {
            setRelayIds(s => {
              const next = new Set(s);
              next.delete(id);
              return next;
            });
          }}
          onPropose={async (ids) => {
            const r = await onPropose?.(source.id, ids);
            if (r && r.ok) {
              setRelayIds(new Set());
              setRelayOpen(false);
            }
            return r;
          }}
          openDetail={openDetail}
        />
      )}
    </AdminShell>
  );
}

/**
 * 회원 아바타 — photos[0].url 이 있으면 사진, 없으면 등급 색상 + 이름 첫 글자.
 * onClick 지정 시 cursor:zoom-in 으로 사진 라이트박스 열기에 사용.
 */
function MemberPhotoAvatar({ member, size = 56, radius = 14, onClick }) {
  const photo = member && member.photos && member.photos[0];
  const initialUrl = photo && photo.url ? photo.url : null;
  const [failed, setFailed] = React.useState(false);
  React.useEffect(() => { setFailed(false); }, [initialUrl]);
  const showImg = !!initialUrl && !failed;
  const initial = ((member && member.name) || '?').charAt(0);
  const tier = (member && member.tier) || 'white';
  return (
    <div
      onClick={onClick}
      role={onClick ? 'button' : undefined}
      aria-label={onClick && member?.name ? `${member.name} 사진 보기` : undefined}
      style={{
        width: size, height: size, borderRadius: radius,
        background: tierAvatarBg(tier), color: tierAvatarColor(tier),
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: Math.max(12, Math.round(size * 0.4)), fontWeight: 700, flexShrink: 0,
        overflow: 'hidden', position: 'relative',
        cursor: onClick ? 'zoom-in' : 'default',
        border: !showImg && tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
      }}
    >
      {showImg ? (
        <img
          src={initialUrl}
          alt={member?.name || ''}
          loading="lazy"
          onError={() => setFailed(true)}
          style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
        />
      ) : initial}
    </div>
  );
}

/**
 * 회원 사진 스택 — 메인 1장(큼직) + 썸네일 row.
 * 사진이 한 장도 없으면 null 반환 (호출 측에서 폴백 처리).
 */
function MemberPhotoStack({ member, onOpen }) {
  const photos = (member && member.photos) || [];
  if (photos.length === 0) return null;
  const main = photos[0];
  const rest = photos.slice(1);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 14 }}>
      <button
        onClick={() => onOpen?.(0)}
        aria-label="메인 사진 확대"
        style={{
          width: '100%', aspectRatio: '4 / 3', borderRadius: 14,
          background: '#1a1714', position: 'relative', overflow: 'hidden',
          cursor: 'zoom-in', padding: 0, border: 0,
        }}
      >
        <img
          src={main.url}
          alt={(member?.name || '') + ' 메인 사진'}
          loading="lazy"
          style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
        />
        <div style={{
          position: 'absolute', top: 8, left: 8,
          background: 'rgba(22,20,18,0.7)', color: '#fff',
          padding: '3px 9px', borderRadius: 999,
          fontSize: 10, fontFamily: 'var(--f-mono)', letterSpacing: '0.04em',
          backdropFilter: 'blur(4px)',
        }}>사진 {photos.length}장</div>
      </button>
      {rest.length > 0 && (
        <div style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${Math.min(rest.length, 4)}, 1fr)`,
          gap: 6,
        }}>
          {rest.slice(0, 4).map((p, i) => (
            <button
              key={p.accessKey || p.url || i}
              onClick={() => onOpen?.(i + 1)}
              aria-label={`사진 ${i + 2} 확대`}
              style={{
                aspectRatio: '1 / 1', borderRadius: 10, overflow: 'hidden',
                padding: 0, border: 0, cursor: 'zoom-in', background: '#eee',
              }}
            >
              <img
                src={p.url}
                alt=""
                loading="lazy"
                style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
              />
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/**
 * 전체 화면 사진 라이트박스.
 *  - ESC / 배경 클릭 / × 버튼으로 닫기
 *  - ← / → 키 또는 화살표 버튼으로 사진 전환
 */
function PhotoLightbox({ photos, startIndex = 0, onClose }) {
  const [idx, setIdx] = React.useState(startIndex);
  React.useEffect(() => { setIdx(Math.min(startIndex, Math.max(0, (photos || []).length - 1))); }, [startIndex, photos]);
  React.useEffect(() => {
    const len = (photos || []).length;
    const onKey = (e) => {
      if (e.key === 'Escape') onClose?.();
      else if (len > 1 && e.key === 'ArrowRight') setIdx(i => (i + 1) % len);
      else if (len > 1 && e.key === 'ArrowLeft') setIdx(i => (i - 1 + len) % len);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [photos, onClose]);
  if (!photos || photos.length === 0) return null;
  const p = photos[idx] || photos[0];
  const len = photos.length;

  // ⚠️ Portal — 부모 카드의 stacking context(transform/filter 등) 와 무관하게
  //    document.body 에 직접 마운트해 viewport 전체를 덮는다.
  const node = (
    <div
      onClick={onClose}
      style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.92)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        zIndex: 99999, padding: 24, isolation: 'isolate',
      }}
    >
      <img
        onClick={(e) => e.stopPropagation()}
        src={p.url}
        alt=""
        style={{
          maxWidth: '92vw', maxHeight: '86vh', borderRadius: 14,
          objectFit: 'contain', background: '#1a1714',
          boxShadow: '0 30px 80px rgba(0,0,0,0.5)',
        }}
      />
      <button
        onClick={(e) => { e.stopPropagation(); onClose?.(); }}
        aria-label="닫기"
        style={{
          position: 'fixed', top: 18, right: 18,
          width: 40, height: 40, borderRadius: 999,
          background: 'rgba(255,255,255,0.16)', color: '#fff',
          fontSize: 20, lineHeight: 1, backdropFilter: 'blur(8px)',
          zIndex: 1,
        }}
      >×</button>
      {len > 1 && (
        <>
          <button
            onClick={(e) => { e.stopPropagation(); setIdx(i => (i - 1 + len) % len); }}
            aria-label="이전 사진"
            style={{
              position: 'fixed', left: 18, top: '50%', transform: 'translateY(-50%)',
              width: 48, height: 48, borderRadius: 999,
              background: 'rgba(255,255,255,0.16)', color: '#fff',
              fontSize: 26, lineHeight: 1, backdropFilter: 'blur(8px)',
              zIndex: 1,
            }}
          >‹</button>
          <button
            onClick={(e) => { e.stopPropagation(); setIdx(i => (i + 1) % len); }}
            aria-label="다음 사진"
            style={{
              position: 'fixed', right: 18, top: '50%', transform: 'translateY(-50%)',
              width: 48, height: 48, borderRadius: 999,
              background: 'rgba(255,255,255,0.16)', color: '#fff',
              fontSize: 26, lineHeight: 1, backdropFilter: 'blur(8px)',
              zIndex: 1,
            }}
          >›</button>
          <div style={{
            position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)',
            background: 'rgba(255,255,255,0.12)', color: '#fff',
            padding: '6px 12px', borderRadius: 999,
            fontFamily: 'var(--f-mono)', fontSize: 12, letterSpacing: '0.04em',
            zIndex: 1,
          }}>{idx + 1} / {len}</div>
        </>
      )}
    </div>
  );

  return (typeof document !== 'undefined' && typeof ReactDOM !== 'undefined' && ReactDOM.createPortal)
    ? ReactDOM.createPortal(node, document.body)
    : node;
}

/** 매칭 후보 화면 카톡 멘트용 — 성 1글자 제외 이름 (일반적인 한글 2~4자 이름) */
function nameWithoutSurnameForKakao(name) {
  const n = String(name || '').trim();
  if (n.length <= 1) return n;
  return n.slice(1);
}

function buildMatchFirstKakaoIntroText(givenName) {
  const who = givenName || 'OO';
  return [
    '안녕하세요!',
    '컬러 소개팅 개발자 입니다! 앞전에 소개팅 신청 주셔서 연락 드렸습니다.',
    'https://www.instagram.com/color_sogaeting/',
    '',
    `${who}님에게 매칭 차례인데, 시간 괜찮으시면 매칭률 80% 넘는 분 리스트 전달 드릴려고 하는데 괜찮으실까요?~`,
  ].join('\n');
}

function buildMatchProcessExplainKakaoText(givenName) {
  const who = givenName || 'OO';
  return [
    '진행 방식 부터 소개 드리겠습니다.',
    '',
    `${who}님 프로필 기준 + 상대방 기준 조합으로 매칭률 80% 넘는분들의 프로필을 보내 드립니다.`,
    '꼭 한분만 선택 안하셔 되고, 여러분 선택 또는 선택 안하셔도 됩니다.',
    '선택을 안한다고 영구 종료가 아닌 다음에 제안을 또 드릴 예정입니다.',
    '',
    '단, 아직 선택하실 상대방들의 동의를 받기 전이라 모든 정보는 드리지 않고(사진 포함)',
    '- 나이',
    '- 지역',
    '- 키',
    '- 직업',
    '- 흡연',
    '- 한마디',
    '',
    '이렇게 보내드리고 있습니다.  상호 매칭 동의를 한 경우, 두분 모두 사진 전송 드리며 카톡방 생성 해 드리고 있습니다.',
    '',
    '이해 하셨고 진행 원하시면 말씀 주세요 :)',
  ].join('\n');
}

/** 매칭 진행 화면 좌측 본인 카드 — 첫 인사 / 진행 방식 카톡 복사 */
function MatchSelfKakaoCopyButtons({ member }) {
  const [copied, setCopied] = React.useState(null); // 'first' | 'process' | null
  const given = nameWithoutSurnameForKakao(member?.name);
  const copy = async (kind, text) => {
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
      setCopied(kind);
      window.__adminNotify?.(
        '복사됨',
        kind === 'first' ? '첫 카톡인사가 클립보드에 복사됐어요.' : '진행 방식 카톡이 복사됐어요.',
        2200,
      );
      setTimeout(() => setCopied((c) => (c === kind ? null : c)), 2000);
    } catch (_) {
      window.__adminNotify?.('복사 실패', '브라우저에서 클립보드 권한을 확인해 주세요.', 2800);
    }
  };
  const btnBase = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: 4,
    padding: '5px 10px',
    borderRadius: 8,
    fontSize: 11,
    fontWeight: 700,
    cursor: 'pointer',
    border: '1px solid var(--line)',
    flexShrink: 0,
    fontFamily: 'var(--f-sans)',
  };
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }} onClick={(e) => e.stopPropagation()}>
      <button
        type="button"
        onClick={() => copy('first', buildMatchFirstKakaoIntroText(given))}
        title="첫 카톡 인사 멘트 (줄바꿈 유지)"
        style={{
          ...btnBase,
          background: copied === 'first' ? '#14855a' : '#fff',
          color: copied === 'first' ? '#fff' : 'var(--ink-800)',
          borderColor: copied === 'first' ? '#14855a' : 'var(--line)',
        }}
      >{copied === 'first' ? '복사됨' : '첫 카톡인사'}</button>
      <button
        type="button"
        onClick={() => copy('process', buildMatchProcessExplainKakaoText(given))}
        title="진행 방식 설명 멘트 (줄바꿈 유지)"
        style={{
          ...btnBase,
          background: copied === 'process' ? '#14855a' : '#161412',
          color: '#fff',
          borderColor: '#161412',
        }}
      >{copied === 'process' ? '복사됨' : '진행 방식 카톡'}</button>
    </div>
  );
}

/** 매칭 목록/UI용 키 표기 — 숫자면 cm, 없으면 null */
function formatMemberHeightCm(m) {
  if (!m || m.height == null || m.height === '') return null;
  const n = Number(m.height);
  if (!Number.isFinite(n)) return null;
  return `${n}cm`;
}

function MatchMemberCard({ m, role, openDetail }) {
  const [lightboxAt, setLightboxAt] = React.useState(-1);
  const photos = (m && m.photos) || [];
  return (
    <div className="adm-card" style={{ padding: 18 }}>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10,
        flexWrap: 'wrap', gap: 8,
      }}>
        <div className="tiny">{role}</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <TierChip tier={m.tier} />
          {role === '본인' && <MatchSelfKakaoCopyButtons member={m} />}
        </div>
      </div>
      <div style={{ marginBottom: 14 }} onClick={(e) => e.stopPropagation()}>
        <MemberProfilePhotoDownloadButtons member={m} />
      </div>

      {/* 사진 — 메인 1장 + 썸네일 row. 사진 없으면 폴백 아바타 */}
      {photos.length > 0 ? (
        <MemberPhotoStack member={m} onOpen={(i) => setLightboxAt(i)} />
      ) : null}

      <div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 14 }}>
        <MemberPhotoAvatar
          member={m}
          size={64}
          radius={16}
          onClick={photos.length > 0 ? () => setLightboxAt(0) : undefined}
        />
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 19, fontWeight: 700, letterSpacing: '-0.015em' }}>
            <MemberNameLink member={m} openDetail={openDetail} />
          </div>
          <div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 2 }}>
            {m.age}, {m.gender === 'M' ? '남' : '여'} · {m.mbti}
          </div>
        </div>
      </div>
      <div style={{ display: 'grid', gap: 8, fontSize: 12.5 }}>
        <MiniRow k="직무/회사" v={`${m.role} · ${m.company}`} />
        <MiniRow k="거주" v={m.region.split(' ').slice(0, 2).join(' ')} />
        <MiniRow k="키" v={formatMemberHeightCm(m) || '—'} />
        <MiniRow k="연봉" v={m.salary} mono />
        <MiniRow k="흡연" v={m.smoke} />
        <MiniRow k="결혼희망" v={m.marry} />
        <MiniRow k="카카오" v={m.kakao || '—'} mono copy />
        <MiniRow k="연락처" v={m.contact || '—'} mono copy />
      </div>
      <div style={{
        marginTop: 12, padding: 10, background: 'var(--bg-soft)',
        borderRadius: 10, fontSize: 12, color: 'var(--ink-700)', lineHeight: 1.5,
      }}>
        <div className="tiny" style={{ marginBottom: 4 }}>희망 조건</div>
        <div>나이 <b>{m.prefAge[0]}-{m.prefAge[1]}</b> · 등급 {(m.prefTier || []).map(t => TIER_LABEL[t]).join(', ') || '상관없음'}</div>
        <div style={{ color: 'var(--ink-500)', marginTop: 2 }}>
          지역 {m.prefRegionOpen ? '상관없음' : ((m.prefRegions && m.prefRegions.length > 0) ? m.prefRegions.join(', ') : (m.prefRegion || '—'))}
        </div>
        <div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px solid var(--line)' }}>
          <div className="tiny" style={{ marginBottom: 4 }}>이상형</div>
          <div style={{ color: 'var(--ink-700)', whiteSpace: 'pre-wrap' }}>
            {String(m.ideal || '').trim() || '—'}
          </div>
        </div>
      </div>

      {lightboxAt >= 0 && photos.length > 0 && (
        <PhotoLightbox
          photos={photos}
          startIndex={lightboxAt}
          onClose={() => setLightboxAt(-1)}
        />
      )}
    </div>
  );
}

function MiniRow({ k, v, mono, copy }) {
  const [copied, setCopied] = React.useState(false);
  const text = typeof v === 'string' ? v : String(v ?? '');
  const canCopy = copy && text && text !== '—';
  const onCopy = async (e) => {
    e?.stopPropagation();
    if (!canCopy) return;
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch (_) { /* noop */ }
  };
  return (
    <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
      <div style={{ width: 64, color: 'var(--ink-500)', flexShrink: 0 }}>{k}</div>
      <div
        title={canCopy ? '클릭해서 복사' : undefined}
        onClick={canCopy ? onCopy : undefined}
        style={{
          flex: 1, minWidth: 0,
          fontFamily: mono ? 'var(--f-mono)' : 'inherit',
          fontWeight: 500,
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
          cursor: canCopy ? 'pointer' : 'default',
          display: 'flex', alignItems: 'center', gap: 6,
        }}
      >
        <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{v}</span>
        {canCopy && (
          <span
            style={{
              fontFamily: 'var(--f-mono)', fontSize: 9, fontWeight: 700,
              padding: '2px 6px', borderRadius: 4,
              background: copied ? '#14855a' : 'var(--bg-soft)',
              color: copied ? '#fff' : 'var(--ink-500)',
              letterSpacing: '0.04em', flexShrink: 0,
            }}
          >{copied ? '복사됨' : '복사'}</span>
        )}
      </div>
    </div>
  );
}

function CompatibilityBar({ breakdown }) {
  return (
    <div style={{ display: 'flex', gap: 4, marginTop: 8, flexWrap: 'wrap' }}>
      {breakdown.map(b => {
        const pct = b.s / b.max;
        const color = pct >= 0.8 ? '#14855a' : pct >= 0.5 ? '#f5c419' : pct >= 0.25 ? '#ec4438' : '#d4cdc0';
        return (
          <div key={b.k} title={`${b.k}: ${b.note} (${b.s}/${b.max})`} style={{
            display: 'inline-flex', alignItems: 'center', gap: 4,
            padding: '2px 8px', borderRadius: 999, background: 'var(--bg-soft)',
            fontSize: 10, color: 'var(--ink-700)', fontFamily: 'var(--f-mono)',
          }}>
            <span style={{ width: 5, height: 5, borderRadius: 999, background: color }} />
            {b.k}
          </div>
        );
      })}
    </div>
  );
}

function MatchCompatPanel({ source, candidate, onConfirm, isInRelay, onToggleRelay, openDetail }) {
  const [lightboxAt, setLightboxAt] = React.useState(-1);
  const photos = (candidate && candidate.photos) || [];
  return (
    <div>
      <div className="adm-card" style={{
        background: 'linear-gradient(180deg, #161412, #2a2522)',
        color: '#fff', border: 0, position: 'relative', overflow: 'hidden',
      }}>
        <div className="tiny" style={{ color: 'rgba(255,255,255,0.5)' }}>호환도</div>
        <div style={{ marginTop: 10, display: 'flex', alignItems: 'baseline', gap: 6 }}>
          <div style={{ fontSize: 56, fontWeight: 700, lineHeight: 1, letterSpacing: '-0.03em' }}>
            {candidate.score}
          </div>
          <div style={{ fontSize: 18, color: 'rgba(255,255,255,0.5)' }}>/ 100</div>
        </div>
        <div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 10 }}>
          {candidate.breakdown.map(b => {
            const pct = (b.s / b.max) * 100;
            return (
              <div key={b.k}>
                <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, marginBottom: 4 }}>
                  <span style={{ fontWeight: 600 }}>{b.k}</span>
                  <span style={{ fontFamily: 'var(--f-mono)', color: 'rgba(255,255,255,0.7)' }}>
                    {b.s} / {b.max}
                  </span>
                </div>
                <div style={{ height: 4, background: 'rgba(255,255,255,0.1)', borderRadius: 999, overflow: 'hidden' }}>
                  <div style={{
                    height: '100%', width: `${pct}%`,
                    background: pct >= 80 ? '#5ad17a' : pct >= 50 ? '#f5c419' : '#ec4438',
                    borderRadius: 999, transition: 'width 400ms ease',
                  }} />
                </div>
                <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.55)', marginTop: 4, whiteSpace: 'pre-line', lineHeight: 1.45 }}>{b.note}</div>
              </div>
            );
          })}
        </div>
      </div>

      <div className="adm-card" style={{ marginTop: 12, padding: 16 }}>
        <div className="tiny">선택한 후보</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 10 }}>
          <MemberPhotoAvatar
            member={candidate}
            size={48}
            radius={12}
            onClick={photos.length > 0 ? () => setLightboxAt(0) : undefined}
          />
          <div>
            <div style={{ fontSize: 16, fontWeight: 700 }}>
              <MemberNameLink member={candidate} openDetail={openDetail} />
              <span style={{ color: 'var(--ink-500)', fontWeight: 500, fontSize: 13, marginLeft: 6 }}>{candidate.age}</span>
            </div>
            <div style={{ fontSize: 12.5, color: 'var(--ink-500)', marginTop: 2 }}>
              {[candidate.job, formatMemberHeightCm(candidate)].filter(Boolean).join(' · ')}
            </div>
          </div>
        </div>

        {/* 후보 사진 미니 그리드 — 최대 5장. 카드 폭 안에 균등하게 */}
        {photos.length > 0 && (
          <div style={{
            marginTop: 12, display: 'grid',
            gridTemplateColumns: `repeat(${Math.min(photos.length, 5)}, 1fr)`, gap: 6,
          }}>
            {photos.slice(0, 5).map((p, i) => (
              <button
                key={p.accessKey || p.url || i}
                onClick={() => setLightboxAt(i)}
                aria-label={`사진 ${i + 1} 확대`}
                style={{
                  aspectRatio: '1 / 1', borderRadius: 10, overflow: 'hidden',
                  padding: 0, border: i === 0 ? '1.5px solid #ec4438' : '1px solid var(--line)',
                  cursor: 'zoom-in', background: '#eee', position: 'relative',
                }}
              >
                <img
                  src={p.url}
                  alt=""
                  loading="lazy"
                  style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
                />
                {i === 0 && (
                  <span style={{
                    position: 'absolute', top: 4, left: 4,
                    background: '#ec4438', color: '#fff',
                    fontFamily: 'var(--f-mono)', fontSize: 8, fontWeight: 700,
                    padding: '1px 5px', borderRadius: 4, letterSpacing: '0.04em',
                  }}>MAIN</span>
                )}
              </button>
            ))}
          </div>
        )}

        <div style={{ marginTop: 10 }} onClick={(e) => e.stopPropagation()}>
          <MemberProfilePhotoDownloadButtons member={candidate} compact />
        </div>

        <div style={{
          marginTop: 12, padding: 10, background: 'var(--bg-soft)',
          borderRadius: 10, fontSize: 12, color: 'var(--ink-700)', lineHeight: 1.5,
        }}>
          <div style={{ color: 'var(--ink-500)', fontSize: 11, marginBottom: 4 }}>자기소개</div>
          {candidate.intro}
        </div>
      </div>

      <button
        className={isInRelay ? 'btn ghost full' : 'btn brand full'}
        onClick={onToggleRelay}
        style={{
          marginTop: 12, height: 56,
          border: isInRelay ? '1.5px solid #ec4438' : undefined,
          color: isInRelay ? '#ec4438' : undefined,
          fontWeight: 700,
        }}
      >
        {isInRelay
          ? '✓ 후보로 선택됨 — 다시 누르면 제외'
          : '이 회원을 매칭 후보로 추가 +'}
      </button>
      <div className="tiny" style={{ textAlign: 'center', marginTop: 8, textTransform: 'none', letterSpacing: 0, fontFamily: 'var(--f-sans)' }}>
        선택을 모두 마친 뒤 상단 <b>"당사자에게 리스트 전달"</b>로 한 번에 제안
      </div>

      {lightboxAt >= 0 && photos.length > 0 && (
        <PhotoLightbox
          photos={photos}
          startIndex={lightboxAt}
          onClose={() => setLightboxAt(-1)}
        />
      )}
    </div>
  );
}

function ConfirmMatchModal({ source, candidate, onCancel, onConfirm, openDetail }) {
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      zIndex: 200, padding: 16,
      animation: 'fadeIn 200ms ease',
    }}>
      <div style={{
        background: '#fff', borderRadius: 22, width: '100%', maxWidth: 460,
        padding: 28, animation: 'pop 220ms ease',
      }}>
        <div className="tiny">매칭 확정</div>
        <div style={{ fontSize: 24, fontWeight: 700, marginTop: 6, letterSpacing: '-0.02em' }}>
          두 분을 매칭 진행할게요
        </div>
        <div style={{
          marginTop: 22, display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          background: 'var(--bg-soft)', padding: 18, borderRadius: 16,
        }}>
          <MiniMember m={source} openDetail={openDetail} />
          <div style={{
            fontFamily: 'var(--f-serif)', fontStyle: 'italic',
            fontSize: 26, color: 'var(--brand)',
          }}>×</div>
          <MiniMember m={candidate} openDetail={openDetail} />
        </div>
        <div style={{
          marginTop: 16, padding: 14, background: '#fff7ec',
          border: '1px solid #f3e3b8', borderRadius: 12,
          fontSize: 13, color: '#8c6a05', lineHeight: 1.55,
        }}>
          💌 확정 시 다음 작업이 자동 수행됩니다:<br />
          · 양쪽 카톡으로 매칭 안내 발송<br />
          · 매칭 기록 저장 (대시보드 즉시 반영)<br />
          · 양쪽 회원 상태가 '매칭 진행 중'으로 변경
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 8, marginTop: 18 }}>
          <button className="btn ghost" onClick={onCancel} style={{ height: 50 }}>취소</button>
          <button className="btn brand" onClick={onConfirm} style={{ height: 50 }}>매칭 확정</button>
        </div>
      </div>
      <style>{`@keyframes fadeIn { from { opacity: 0; } } @keyframes pop { from { opacity: 0; transform: scale(0.96); } }`}</style>
    </div>
  );
}

function MiniMember({ m, openDetail }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
      <MemberPhotoAvatar member={m} size={44} radius={12} />
      <div>
        <div style={{ fontSize: 14, fontWeight: 700 }}>
          <MemberNameLink member={m} openDetail={openDetail} />
        </div>
        <div style={{ fontSize: 11, color: 'var(--ink-500)' }}>{m.age}, {m.gender === 'M' ? '남' : '여'} · {TIER_LABEL[m.tier]}</div>
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// 당사자에게 카톡으로 보낼 후보 리스트 템플릿 모달
//   - 보내는 정보: 등급(색상), MBTI, 흡연/결혼희망/학력, 자기소개 한 줄
//   - 빼는 정보:   나이 / 지역 / 키 / 직업 / 연봉 / 연락처
//   (의사 확인 후 자세한 정보 공유)
// ═════════════════════════════════════════════════════════════
function maskName(name, mode) {
  if (!name) return '';
  if (mode === 'full') return name;
  // 이니셜 마스킹 — 첫 글자만 보이고 나머지는 '*' (예: 김도윤 → 김**, 양주은 → 양**)
  if (name.length <= 1) return name;
  return name[0] + '*'.repeat(name.length - 1);
}

function shortIntro(text, max = 80) {
  if (!text) return '';
  const t = String(text).replace(/\s+/g, ' ').trim();
  return t.length > max ? t.slice(0, max - 1) + '…' : t;
}

/**
 * 카톡 멘트용 한마디 — 잘라내지 않고 전체 텍스트 사용.
 * 줄바꿈/탭/연속 공백만 단일 공백으로 정리해서 한 줄에 자연스럽게 들어가게 한다.
 */
function fullIntro(text) {
  if (!text) return '';
  return String(text).replace(/\s+/g, ' ').trim();
}

/**
 * 카톡 템플릿의 「나이」 표기 — 출생년도(만나이) 형식.
 *  - 1997년생 28세 → "1997(28)"
 *  - birthYear만 있고 age 없음 → "1997"
 *  - age만 있고 birthYear 없음 → "28"
 *  - 둘 다 없음 → '' (호출부에서 라인 자체를 생략)
 */
function formatAgeWithBirth(m) {
  if (!m) return '';
  const by = Number(m.birthYear);
  const age = m.age;
  const hasBy = Number.isFinite(by) && by > 0;
  const hasAge = typeof age === 'number' ? Number.isFinite(age) : !!age;
  if (hasBy && hasAge) return `${by}(${age})`;
  if (hasBy) return String(by);
  if (hasAge) return String(age);
  return '';
}

function buildRelayMessage({ source, candidates, options }) {
  const { nameMode = 'mask', includeIntro = true } = options || {};
  const lines = [];
  lines.push(`안녕하세요 ${source.name}님, 컬러소개팅입니다.`);
  lines.push('');
  lines.push(`${source.name}님께 잘 어울릴 만한 분 ${candidates.length}명 추려서 먼저 보내드려요. 부담 갖지 마시고 한번 봐주시면 됩니다. 꼭 1명만 보낼 필요는 없어요. 여러 명을 함께내도 되고, 이번에는 0명(아무도 안 보냄)도 괜찮아요.
이번에 아무도 안 고른다고 해서 소개가 멈추거나 정지되는 건 아니에요. 다음에 조건에 맞는 분이 생기면 또 추가로 자동으로 이어져요.`);
  lines.push('');

  candidates.forEach((c, i) => {
    const num = `[${i + 1}]`;
    const displayName = maskName(c.name, nameMode);
    lines.push(`${num} ${displayName}`);
    if (c.tier) lines.push(`    등급: ${TIER_LABEL[c.tier] || c.tier}`);
    const ageStr = formatAgeWithBirth(c);
    if (ageStr) lines.push(`    나이: ${ageStr}`);
    if (c.region) lines.push(`    지역: ${c.region}`);
    if (typeof c.height === 'number' || c.height) lines.push(`    키: ${c.height}cm`);
    if (c.role) lines.push(`    직업: ${c.role}`);
    if (c.smoke) lines.push(`    흡연: ${c.smoke}`);
    if (includeIntro && c.intro) lines.push(`    한마디: ${fullIntro(c.intro)}`);
    lines.push('');
  });

  lines.push('이 중에 마음에 드시는 분이 있다면 번호로 알려주세요.');
  lines.push('자세한 정보(회사·연봉·연락처 등)는 의사 확인 후에 안내드릴게요.');
  lines.push('이번 소개는 패스 하시려면 꼭 말씀 부탁 드립니다.');
  return lines.join('\n');
}

/**
 * 매칭 기록 트리 — 한 소스 회원이 받은 모든 제안 그룹을 트리 형태로 표시.
 *
 *   양주은  ─┬─ ○ 윤성원  (최종)
 *           ├─ ○ 임재우  (최종)
 *           ├─ ? 조은수   (대기)
 *           └─ ✗ 옥영빈  (미선택)
 *           [그룹 #ab12cd  ·  3일 전  ·  후보 4명 / 최종 2명]
 *
 *   같은 회원에게 여러 번 제안된 경우 그룹 단위로 여러 카드.
 *   각 후보 우측 액션: '최종 선택' (proposed 상태일 때만) — 행마다 독립, 복수 최종 가능.
 */
/**
 * 문구 관리(MESSAGES)의 「선택 알림」과 동일 로직 — 선택받은 사람(recipient)에게,
 * 선택한 사람(picker) 정보를 익명 요약으로 실어 보냄 (buildSelectedNotifyMessage).
 * 매칭 트리에서는 recipient=후보, picker=제안 받은 주인공(소스).
 */
function NotifyMessageCopyChip({ recipient, picker }) {
  const [copied, setCopied] = React.useState(false);
  const canCopy = !!(recipient && picker);
  const onClick = async (e) => {
    e?.stopPropagation();
    if (!canCopy) return;
    const message = buildSelectedNotifyMessage({
      source: recipient,
      chooser: picker,
      options: { nameMode: 'mask', includeIntro: true },
    });
    if (!message) return;
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(message);
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {}
  };
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={!canCopy}
      title={canCopy ? `${recipient?.name || ''}님(선택받은 분)께 보낼 선택 알림 멘트 복사` : '회원 정보가 필요합니다'}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 4,
        padding: '2px 9px', borderRadius: 999,
        background: copied ? '#14855a' : '#fde7e3',
        color: copied ? '#fff' : '#b53026',
        border: copied ? '0' : '1px solid #f5c0b8',
        fontSize: 10.5, fontWeight: 700, cursor: canCopy ? 'pointer' : 'not-allowed',
        fontFamily: 'var(--f-sans)', letterSpacing: '0.02em',
        opacity: canCopy ? 1 : 0.5, flexShrink: 0,
      }}
    >
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
        <path d="M21 11.5a8.5 8.5 0 11-2.5-6L21 3v6h-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      </svg>
      {copied ? '복사됨' : '선택 알림 복사'}
    </button>
  );
}

/**
 * 주인공에게 매칭 진행 방식(사진 공개 방식)을 안내하는 멘트.
 *  - 후보를 한 명 선택 → 선택받은 사람에게 주인공 프로필 먼저 전달
 *  - 양쪽 의사 확인 → 사진 교환 → 최종 매칭 → 3자 카톡방 생성
 *  - 사진은 양쪽 동의가 있을 때만 공유된다는 점 강조
 */
function buildPhotoDisclosureMessage({ source }) {
  if (!source) return '';
  const name = source.name || '회원';
  const lines = [];
  lines.push(`이제, 다음 순서 안내 드리겠습니다.`);
  lines.push('');
  lines.push(`2) 선택받은 분께 먼저 ${name}님의 프로필(사진,기타 민감 개인 정보 제외)을 전달드립니다.`);
  lines.push(`3) 그분도 진행 의사가 있으시면, 양쪽 동의 하에 그때 사진을 서로 교환해드려요.`);
  lines.push(`4) 사진까지 확인하시고 두 분 모두 매칭을 희망하시면 3자 카톡방을 만들어드립니다.`);
  lines.push('');
  lines.push('사진은 양쪽 동의가 있을 때만 공유되니 부담 없이 진행하셔도 됩니다.');
  lines.push('선택하신 분들께 프로필, 사진 전송 여부 편하게 답변 주세요. 감사합니다 :)');
  return lines.join('\n');
}

/** 주인공 행에 붙는 칩 — 사진 공개 방식 안내 멘트 복사 */
function PhotoDisclosureCopyChip({ source }) {
  const [copied, setCopied] = React.useState(false);
  const canCopy = !!source;
  const onClick = async (e) => {
    e?.stopPropagation();
    if (!canCopy) return;
    const message = buildPhotoDisclosureMessage({ source });
    if (!message) return;
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(message);
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {}
  };
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={!canCopy}
      title={canCopy ? `${source?.name || ''}님께 보낼 「사진 공개 방식 안내」 멘트 복사` : '회원 정보가 필요합니다'}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 4,
        padding: '2px 9px', borderRadius: 999,
        background: copied ? '#14855a' : '#eef4ec',
        color: copied ? '#fff' : '#2f6b4a',
        border: copied ? '0' : '1px solid #c8e0d0',
        fontSize: 10.5, fontWeight: 700, cursor: canCopy ? 'pointer' : 'not-allowed',
        fontFamily: 'var(--f-sans)', letterSpacing: '0.02em',
        opacity: canCopy ? 1 : 0.5, flexShrink: 0,
      }}
    >
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
        <path d="M21 11.5a8.5 8.5 0 11-2.5-6L21 3v6h-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      </svg>
      {copied ? '복사됨' : '사진 공개 방식 안내'}
    </button>
  );
}

/**
 * 사진 교환 동의 안내 멘트.
 *  - 양쪽이 진행 의사를 표한 후, 사진 교환 단계로 넘어가기 전 안내
 *  - 한 쪽이라도 비공개를 원하면 사진 공유 안 됨
 *  - 양쪽 사진 확인 후 최종 의사 → 3자 카톡방 생성
 */
function buildPhotoExchangeMessage({ source, candidate }) {
  if (!source || !candidate) return '';
  const sName = source.name || '주인공';
  const cName = candidate.name || '회원';
  const lines = [];
  lines.push(`안녕하세요 ${cName}님, 컬러소개팅입니다.`);
  lines.push('');
  lines.push(`좋은 소식이에요. ${sName}님께서도 ${cName}님과 진행해보고 싶다고 답을 주셨습니다.`);
  lines.push('');
  lines.push('다음 단계로 양쪽이 서로의 사진을 교환할 예정인데, 사진 공유는 양쪽 모두 동의가 있어야만 진행됩니다. 한 분이라도 비공개를 원하시면 사진은 공유되지 않으니 편하게 의사 말씀 주세요.');
  lines.push('');
  lines.push('사진 교환 후 두 분 모두 최종 진행 의사가 확인되면, 그때 3자 카톡방을 만들어드릴 예정입니다.');
  lines.push('');
  lines.push('사진 교환 진행에 동의하실지 답변 부탁드려요. 감사합니다 :)');
  return lines.join('\n');
}

/** 후보 행에 붙는 칩 — 사진 교환 동의 안내 멘트 복사 */
function PhotoExchangeCopyChip({ source, candidate }) {
  const [copied, setCopied] = React.useState(false);
  const canCopy = !!(source && candidate);
  const onClick = async (e) => {
    e?.stopPropagation();
    if (!canCopy) return;
    const message = buildPhotoExchangeMessage({ source, candidate });
    if (!message) return;
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(message);
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {}
  };
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={!canCopy}
      title={canCopy ? `${candidate?.name || ''}님께 보낼 「사진 교환 동의」 멘트 복사` : '회원 정보가 필요합니다'}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 4,
        padding: '2px 9px', borderRadius: 999,
        background: copied ? '#14855a' : '#fff4cf',
        color: copied ? '#fff' : '#8c6a05',
        border: copied ? '0' : '1px solid #f3e3b8',
        fontSize: 10.5, fontWeight: 700, cursor: canCopy ? 'pointer' : 'not-allowed',
        fontFamily: 'var(--f-sans)', letterSpacing: '0.02em',
        opacity: canCopy ? 1 : 0.5, flexShrink: 0,
      }}
    >
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
        <path d="M21 11.5a8.5 8.5 0 11-2.5-6L21 3v6h-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      </svg>
      {copied ? '복사됨' : '사진 교환 동의'}
    </button>
  );
}

/** 3자 카톡방 첫 안내 — buildMatchedRoomMessage (문구 관리의 카톡방 매칭 안내와 동일 생성기) */
function MatchRoomGreetingCopyChip({ memberA, memberB }) {
  const [copied, setCopied] = React.useState(false);
  const canCopy = !!(memberA && memberB);
  const onClick = async (e) => {
    e?.stopPropagation();
    if (!canCopy) return;
    const message = buildMatchedRoomMessage({
      memberA,
      memberB,
      options: { includeIntro: true },
    });
    if (!message) return;
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(message);
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {}
  };
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={!canCopy}
      title={canCopy ? '최종 매칭 후 3자 카톡방에 붙여넣을 안내 멘트 복사' : '회원 정보가 필요합니다'}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 4,
        padding: '2px 9px', borderRadius: 999,
        background: copied ? '#14855a' : '#e8f0ff',
        color: copied ? '#fff' : '#1e4cb6',
        border: copied ? '0' : '1px solid #c8d8f5',
        fontSize: 10.5, fontWeight: 700, cursor: canCopy ? 'pointer' : 'not-allowed',
        fontFamily: 'var(--f-sans)', letterSpacing: '0.02em',
        opacity: canCopy ? 1 : 0.5, flexShrink: 0,
      }}
    >
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
        <path d="M21 11.5a8.5 8.5 0 11-2.5-6L21 3v6h-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      </svg>
      {copied ? '복사됨' : '최종 카톡 문구 복사'}
    </button>
  );
}

/** 카톡 ID / 연락처 칩 — 클릭 한 번에 복사 */
function ContactChip({ kind, value }) {
  const [copied, setCopied] = React.useState(false);
  const text = String(value || '').trim();
  if (!text) return null;
  const isKakao = kind === '카톡';
  const isInsta = kind === '인스타';
  const onClick = async (e) => {
    e?.stopPropagation();
    try {
      const payload = isInsta ? text.replace(/^@+/, '') : text;
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(payload);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch (_) {}
  };
  return (
    <button
      type="button"
      onClick={onClick}
      title={`${kind} 복사: ${text}`}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 4,
        padding: '2px 8px', borderRadius: 999,
        background: copied
          ? '#14855a'
          : isKakao ? '#fff4cf'
          : isInsta ? '#fdf2fa'
          : 'var(--bg-soft)',
        color: copied ? '#fff' : isKakao ? '#8c6a05' : isInsta ? '#8e2a7b' : 'var(--ink-700)',
        border: copied ? '0' : '1px solid ' + (isKakao ? '#f3e3b8' : isInsta ? '#e6c4df' : 'var(--line)'),
        fontSize: 10.5, fontWeight: 600, cursor: 'pointer',
        fontFamily: 'var(--f-mono)', letterSpacing: '0.02em',
        maxWidth: '100%',
      }}
    >
      <span style={{ fontSize: 9, opacity: 0.7 }}>{kind}</span>
      <span style={{
        fontWeight: 700,
        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
        maxWidth: 180,
      }}>{copied ? '복사됨' : text}</span>
    </button>
  );
}

function MatchProposalsHistory({ source, proposalGroups, onSelectFinal, onReject, hideHeader = false, members, openDetail }) {
  const groups = Array.isArray(proposalGroups) ? proposalGroups : [];

  // c.member 는 백엔드 populate 결과(최소 필드)라 카톡/연락처가 없음.
  // 클라이언트의 회원 메모리에서 _id 로 풀 정보를 보강한다.
  const memberById = React.useMemo(() => {
    const map = new Map();
    (members || []).forEach((m) => {
      if (!m) return;
      if (m._id) map.set(String(m._id), m);
      if (m.id) map.set(String(m.id), m);
    });
    return map;
  }, [members]);
  const enrich = (m) => {
    if (!m) return m;
    const k = String(m._id || m.id || '');
    const full = memberById.get(k);
    if (!full) return m;
    const photos = (m.photos && m.photos.length > 0) ? m.photos : (full.photos || m.photos || []);
    return {
      ...full,
      ...m,
      kakao: full.kakao,
      contact: full.contact,
      insta: full.insta,
      role: full.role,
      region: full.region,
      photos,
    };
  };

  const formatGroupId = (gid) =>
    typeof gid === 'string' ? gid.slice(0, 8) : String(gid || '').slice(0, 8);

  const formatTimeAgo = (iso) => {
    if (!iso) return '';
    const ms = Date.now() - new Date(iso).getTime();
    if (ms < 60_000) return '방금';
    if (ms < 3600_000) return `${Math.floor(ms / 60_000)}분 전`;
    if (ms < 86_400_000) return `${Math.floor(ms / 3600_000)}시간 전`;
    return `${Math.floor(ms / 86_400_000)}일 전`;
  };

  const statusMeta = (s) => {
    if (s === 'final') return { color: '#1e4cb6', bg: '#e3edff', label: '최종 ✓', icon: 'O' };
    if (s === 'success') return { color: '#14855a', bg: '#e8f7ee', label: '성사', icon: 'O' };
    if (s === 'fail') return { color: '#a8281d', bg: '#fee5e2', label: '실패', icon: 'X' };
    if (s === 'rejected') return { color: '#8c6a05', bg: '#fff4cf', label: '미선택', icon: 'X' };
    if (s === 'ongoing') return { color: '#1e4cb6', bg: '#e3edff', label: '진행 중', icon: 'O' };
    return { color: 'var(--ink-500)', bg: 'var(--bg-soft)', label: '대기', icon: '?' };
  };

  return (
    <div className="adm-card" style={{ marginTop: hideHeader ? 0 : 18, padding: hideHeader ? 16 : 22 }}>
      {!hideHeader && (
        <div style={{
          display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
          gap: 10, flexWrap: 'wrap', marginBottom: 14,
        }}>
          <div>
            <div className="tiny">MATCH RECORDS · <MemberNameLink member={source} openDetail={openDetail} />님의 매칭 제안 기록</div>
            <div className="chart-title" style={{ marginTop: 4 }}>
              받은 제안 {groups.length}건
              {groups.length > 0 && (
                <span style={{
                  fontSize: 13, fontWeight: 500, color: 'var(--ink-500)', marginLeft: 8,
                }}>
                  · 후보 {groups.reduce((s, g) => s + (g.candidates?.length || 0), 0)}명 ·{' '}
                  최종 {groups.reduce((s, g) =>
                    s + (g.candidates || []).filter(c => c.status === 'final' || c.status === 'success' || c.status === 'ongoing').length, 0
                  )}명
                </span>
              )}
            </div>
          </div>
        </div>
      )}

      {groups.length === 0 ? (
        <div style={{
          padding: 28, textAlign: 'center', color: 'var(--ink-400)', fontSize: 13,
          background: 'var(--bg-soft)', borderRadius: 12,
        }}>
          아직 매칭 제안 기록이 없습니다.<br />
          후보를 골라 <b>"당사자에게 리스트 전달 → 이 제안 등록"</b>으로 첫 제안을 만들어 보세요.
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          {groups.map((g) => {
            const cands = g.candidates || [];
            const finals = cands.filter(c => c.status === 'final' || c.status === 'success' || c.status === 'ongoing');
            const decided = cands.every(c => c.status !== 'proposed');
            return (
              <div
                key={g.groupId}
                style={{
                  border: '1px solid var(--line)', borderRadius: 14,
                  background: '#fff', overflow: 'hidden',
                }}
              >
                {/* 그룹 헤더 */}
                <div style={{
                  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                  padding: '10px 14px', background: 'var(--bg-soft)',
                  borderBottom: '1px solid var(--line)', flexWrap: 'wrap', gap: 8,
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
                    <span style={{
                      fontFamily: 'var(--f-mono)', fontSize: 11, fontWeight: 700,
                      background: '#fff', color: 'var(--ink-700)',
                      padding: '3px 8px', borderRadius: 6,
                      border: '1px solid var(--line)', letterSpacing: '0.04em',
                    }}>#{formatGroupId(g.groupId)}</span>
                    <span style={{ fontSize: 12, color: 'var(--ink-500)' }}>{formatTimeAgo(g.createdAt)}</span>
                    <span style={{ fontSize: 12, color: 'var(--ink-700)', fontWeight: 600 }}>
                      후보 {cands.length}명
                    </span>
                    {finals.length > 0 && (
                      <span style={{
                        background: '#e3edff', color: '#1e4cb6',
                        padding: '2px 8px', borderRadius: 999,
                        fontSize: 11, fontWeight: 700,
                      }}>최종 {finals.length}명</span>
                    )}
                    {!decided && finals.length === 0 && (
                      <span style={{
                        background: '#fff7ec', color: '#8c6a05',
                        padding: '2px 8px', borderRadius: 999,
                        fontSize: 11, fontWeight: 700,
                      }}>응답 대기</span>
                    )}
                  </div>
                </div>

                {/* 트리 본문: 소스 ─┬─ 후보 1 / ├─ 후보 2 / └─ 후보 N */}
                <div style={{ padding: '12px 14px 14px' }}>
                  {/* 소스 노드 */}
                  <div style={{
                    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    gap: 10, marginBottom: 6, flexWrap: 'wrap',
                  }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0, flexWrap: 'wrap' }}>
                      <MemberPhotoAvatar member={source} size={34} radius={10} />
                      <div style={{ fontSize: 14, fontWeight: 700 }}>
                        <MemberNameLink member={source} openDetail={openDetail} />
                        <span style={{ color: 'var(--ink-500)', fontWeight: 500, marginLeft: 6, fontSize: 12 }}>
                          {source.age}, {source.gender === 'M' ? '남' : '여'}
                        </span>
                      </div>
                      <PhotoDisclosureCopyChip source={source} />
                    </div>
                    <MemberProfilePhotoDownloadButtons member={source} compact />
                  </div>

                  {/* 후보 노드들 — 트리 라인은 ASCII가 아닌 CSS border로 */}
                  <div style={{ paddingLeft: 17, position: 'relative' }}>
                    {/* 세로 라인 */}
                    <div style={{
                      position: 'absolute', left: 17, top: 0,
                      bottom: 14, width: 2, background: 'var(--line)',
                    }} />
                    {cands.map((c, idx) => {
                      const meta = statusMeta(c.status);
                      const isFinal = meta.icon === 'O';
                      const isProposed = c.status === 'proposed';
                      const m = enrich(c.member || {});
                      return (
                        <div
                          key={c._id}
                          style={{
                            display: 'flex', alignItems: 'stretch', gap: 12,
                            padding: '8px 0', position: 'relative',
                            paddingLeft: 28,
                          }}
                        >
                          {/* 가로 라인 (└─ 또는 ├─) */}
                          <div style={{
                            position: 'absolute', left: 0, top: 24,
                            width: 22, height: 2, background: 'var(--line)',
                          }} />
                          {/* 마지막 항목이면 세로선 가림 */}
                          {idx === cands.length - 1 && (
                            <div style={{
                              position: 'absolute', left: 0, top: 24,
                              width: 2, bottom: 0, background: '#fff',
                            }} />
                          )}

                          {/* 제안 순번 — 최좌측 */}
                          <div
                            title={`제안 순번 ${idx + 1}`}
                            style={{
                              flexShrink: 0,
                              width: 28,
                              display: 'flex',
                              alignItems: 'center',
                              justifyContent: 'center',
                              alignSelf: 'center',
                            }}
                          >
                            <span style={{
                              fontFamily: 'var(--f-mono)',
                              fontSize: 15,
                              fontWeight: 800,
                              color: 'var(--ink-400)',
                              letterSpacing: '-0.02em',
                            }}>{idx + 1}</span>
                          </div>

                          {/* 결과 아이콘 (O / X) */}
                          <div
                            title={meta.label}
                            style={{
                              width: 28, height: 28, borderRadius: 999, flexShrink: 0,
                              alignSelf: 'center',
                              background: isFinal ? '#1e4cb6' : c.status === 'proposed' ? 'transparent' : '#ec4438',
                              color: '#fff',
                              border: c.status === 'proposed' ? '2px dashed var(--ink-400)' : '0',
                              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                              fontSize: 14, fontWeight: 800, lineHeight: 1,
                            }}
                          >
                            {isFinal ? '○' : c.status === 'proposed' ? '?' : '✕'}
                          </div>

                          {/* 후보 카드 미니 */}
                          <div style={{
                            display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0,
                            padding: '6px 10px', borderRadius: 10,
                            background: isFinal ? '#f0f5ff' : 'transparent',
                            border: isFinal ? '1px solid #c8d8f5' : '1px solid transparent',
                          }}>
                            <MemberPhotoAvatar member={m} size={32} radius={9} />
                            <div style={{ flex: 1, minWidth: 0 }}>
                              <div style={{ fontSize: 13, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                                <MemberNameLink member={m} openDetail={openDetail} />
                                <span style={{ color: 'var(--ink-500)', fontWeight: 500, fontSize: 11 }}>
                                  {m.age ? `${m.age}` : ''}{m.gender ? ` · ${m.gender === 'M' ? '남' : '여'}` : ''}
                                </span>
                                <span style={{
                                  background: meta.bg, color: meta.color,
                                  padding: '1px 7px', borderRadius: 999,
                                  fontSize: 10, fontWeight: 700, letterSpacing: '0.02em',
                                }}>{meta.label}</span>
                              </div>
                              {typeof c.score === 'number' && c.score > 0 && (
                                <div style={{
                                  fontSize: 10, color: 'var(--ink-500)',
                                  fontFamily: 'var(--f-mono)', marginTop: 2,
                                }}>호환도 {c.score}/100</div>
                              )}
                              {/* 카톡 / 연락처 / 인스타 / 알림 멘트 복사 */}
                              {(m.kakao || m.contact || m.insta || source) && (
                                <div style={{
                                  display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4,
                                  alignItems: 'center',
                                }}>
                                  {m.kakao && <ContactChip kind="카톡" value={m.kakao} />}
                                  {m.contact && <ContactChip kind="연락처" value={m.contact} />}
                                  {m.insta && String(m.insta).trim() && (
                                    <ContactChip kind="인스타" value={m.insta} />
                                  )}
                                  {source && (
                                    <>
                                      <NotifyMessageCopyChip recipient={m} picker={source} />
                                      <PhotoExchangeCopyChip source={source} candidate={m} />
                                      <MatchRoomGreetingCopyChip memberA={source} memberB={m} />
                                    </>
                                  )}
                                </div>
                              )}
                              <div style={{ marginTop: 6 }} onClick={(e) => e.stopPropagation()}>
                                <MemberProfilePhotoDownloadButtons member={m} compact />
                              </div>
                            </div>
                            {isProposed && (onSelectFinal || onReject) && (
                              <div style={{ display: 'inline-flex', gap: 6, flexShrink: 0 }}>
                                {onSelectFinal && (
                                  <button
                                    onClick={() => onSelectFinal(c._id)}
                                    title="이 후보를 최종 매칭으로 승격 (○ 표시)"
                                    style={{
                                      padding: '6px 12px', borderRadius: 999,
                                      background: '#1e4cb6', color: '#fff',
                                      fontSize: 11, fontWeight: 700,
                                      cursor: 'pointer', border: 0,
                                      display: 'inline-flex', alignItems: 'center', gap: 4,
                                    }}
                                  >
                                    <span style={{ fontSize: 13, lineHeight: 1 }}>○</span>
                                    최종 선택
                                  </button>
                                )}
                                {onReject && (
                                  <button
                                    onClick={() => onReject(c._id)}
                                    title="이 후보를 미선택 처리 (✕ 표시)"
                                    style={{
                                      padding: '6px 12px', borderRadius: 999,
                                      background: '#fff', color: '#ec4438',
                                      fontSize: 11, fontWeight: 700,
                                      cursor: 'pointer', border: '1.5px solid #ec4438',
                                      display: 'inline-flex', alignItems: 'center', gap: 4,
                                    }}
                                  >
                                    <span style={{ fontSize: 13, lineHeight: 1, fontWeight: 800 }}>✕</span>
                                    선택 안함
                                  </button>
                                )}
                              </div>
                            )}
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function RelayCandidatesModal({ source, candidates, onClose, onRemove, onPropose, openDetail }) {
  // 기본값은 익명성 보호를 위해 마스킹(예: 김**)
  const [nameMode, setNameMode] = React.useState('mask'); // 'mask' | 'full'
  const [includeIntro, setIncludeIntro] = React.useState(true);
  const [copied, setCopied] = React.useState(false);
  const [proposing, setProposing] = React.useState(false);
  const [proposed, setProposed] = React.useState(false);
  const taRef = React.useRef(null);

  const message = React.useMemo(
    () => buildRelayMessage({ source, candidates, options: { nameMode, includeIntro } }),
    [source, candidates, nameMode, includeIntro],
  );

  const copy = async () => {
    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(message);
      } else if (taRef.current) {
        taRef.current.select();
        document.execCommand('copy');
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {
      // fallback: textarea 자체 선택만
      if (taRef.current) {
        taRef.current.select();
      }
    }
  };

  // ESC로 닫기
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div
      onClick={onClose}
      style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        zIndex: 220, padding: 16,
        animation: 'fadeIn 200ms ease',
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          background: '#fff', borderRadius: 22, width: '100%', maxWidth: 820,
          maxHeight: '92vh', display: 'flex', flexDirection: 'column',
          animation: 'pop 220ms ease', overflow: 'hidden',
        }}
      >
        {/* 헤더 */}
        <div style={{
          padding: '20px 24px 16px', borderBottom: '1px solid var(--line)',
          display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12,
        }}>
          <div>
            <div className="tiny">RELAY · 당사자에게 후보 전달</div>
            <div style={{ fontSize: 22, fontWeight: 700, marginTop: 6, letterSpacing: '-0.02em' }}>
              {source.name}님께 보낼 카톡 멘트
            </div>
            <div style={{ fontSize: 12.5, color: 'var(--ink-500)', marginTop: 4, lineHeight: 1.55 }}>
              아래 내용을 복사해 {source.name}님 카톡으로 그대로 보내세요. 이름은 이니셜로 가리고(예: 김**) 회사·연봉·연락처는 의사 확인 후에 공유됩니다.
            </div>
          </div>
          <button
            onClick={onClose}
            aria-label="닫기"
            style={{
              width: 32, height: 32, borderRadius: 999,
              background: 'var(--bg-soft)', color: 'var(--ink-700)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontSize: 16, flexShrink: 0,
            }}
          >×</button>
        </div>

        {/* 선택 인원·진행 안내 (상단 고정) */}
        <div
          onClick={(e) => e.stopPropagation()}
          style={{
            padding: '12px 24px',
            background: '#fffbf0',
            borderBottom: '1px solid #f3e3b8',
            fontSize: 12.5,
            color: 'var(--ink-800)',
            lineHeight: 1.65,
          }}
        >
          <div>
            <span style={{ fontWeight: 700, color: '#8c6a05' }}>선택 인원</span>
            {' '}꼭 1명만 보낼 필요는 없어요. 여러 명을 함께내도 되고, 이번에는 0명(아무도 안 보냄)도 괜찮아요.
          </div>
          <div style={{ marginTop: 6, color: 'var(--ink-600)' }}>
            이번에 아무도 안 고른다고 해서 소개가 멈추거나 정지되는 건 아니에요. 다음에 조건에 맞는 분이 생기면 또 추가로 자동으로 이어져요.
          </div>
        </div>

        {/* 본문 — 2단 (좌: 미리보기, 우: 선택 후보·옵션) */}
        <div className="relay-grid" style={{
          flex: 1, minHeight: 0, overflow: 'auto',
          display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 0,
        }}>
          {/* Left: 텍스트 미리보기 */}
          <div style={{
            padding: '18px 22px', borderRight: '1px solid var(--line)',
            display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0,
          }}>
            <div style={{
              display: 'flex', alignItems: 'center', justifyContent: 'space-between',
              fontSize: 12, color: 'var(--ink-500)',
            }}>
              <span>카톡 멘트 (수정 가능)</span>
              <span style={{ fontFamily: 'var(--f-mono)' }}>{message.length}자</span>
            </div>
            <textarea
              ref={taRef}
              value={message}
              readOnly
              style={{
                flex: 1, width: '100%', minHeight: 320, resize: 'vertical',
                background: 'var(--bg-soft)', border: '1px solid var(--line)',
                borderRadius: 12, padding: 14,
                fontSize: 13.5, lineHeight: 1.7, color: 'var(--ink-900)',
                fontFamily: 'var(--f-sans)', whiteSpace: 'pre-wrap',
              }}
            />
            <div style={{
              padding: '10px 12px', background: '#fff7ec',
              border: '1px solid #f3e3b8', borderRadius: 10,
              fontSize: 12, color: '#8c6a05', lineHeight: 1.55,
            }}>
              💡 우측 옵션을 바꾸면 멘트가 즉시 갱신됩니다. 그대로 복사 → 카톡 → 붙여넣기 한 번으로 전달 끝.
            </div>
          </div>

          {/* Right: 옵션 + 선택된 후보 목록 */}
          <div style={{
            padding: '18px 22px', display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0,
          }}>
            {/* 옵션 */}
            <div>
              <div className="tiny" style={{ marginBottom: 8 }}>OPTIONS · 표시 옵션</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                <RelayToggle
                  label="이름 표시"
                  options={[
                    { v: 'mask', l: '이니셜 (예: 김**)' },
                    { v: 'full', l: '풀네임' },
                  ]}
                  value={nameMode}
                  onChange={setNameMode}
                />
                <RelayCheckRow
                  label="자기소개 한 줄 포함"
                  checked={includeIntro}
                  onToggle={() => setIncludeIntro((v) => !v)}
                />
              </div>
            </div>

            {/* 보내는 항목 안내 */}
            <div style={{
              padding: '10px 12px', borderRadius: 10,
              background: 'var(--bg-soft)', fontSize: 11.5, color: 'var(--ink-700)',
              lineHeight: 1.6,
            }}>
              <div style={{ fontWeight: 700, marginBottom: 4 }}>지금 단계에서 보내는 정보</div>
              <div>이름(이니셜) · 나이 · 지역 · 키 · 직업 · 흡연 · 한마디</div>
              <div style={{ marginTop: 6, fontWeight: 700, color: 'var(--brand-ink)' }}>아직 보내지 않는 정보</div>
              <div style={{ color: 'var(--brand-ink)' }}>회사 · 연봉 · 연락처 · 카톡/인스타 ID (의사 확인 후 공유)</div>
            </div>

            {/* 선택된 후보 미니 카드 */}
            <div style={{ flex: 1, minHeight: 0 }}>
              <div className="tiny" style={{ marginBottom: 8 }}>
                SELECTED · 선택된 후보 {candidates.length}명
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                {candidates.map((c, i) => (
                  <div
                    key={c.id}
                    style={{
                      display: 'flex', alignItems: 'center', gap: 10,
                      padding: '8px 10px', borderRadius: 12,
                      background: '#fff', border: '1px solid var(--line)',
                    }}
                  >
                    <span style={{
                      width: 18, height: 18, borderRadius: 999,
                      background: 'var(--ink-900)', color: '#fff',
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                      fontSize: 10, fontFamily: 'var(--f-mono)', fontWeight: 700, flexShrink: 0,
                    }}>{i + 1}</span>
                    <MemberPhotoAvatar member={c} size={32} radius={9} />
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 13, fontWeight: 700 }}>
                        <MemberNameLink member={c} openDetail={openDetail} name={maskName(c.name, nameMode)} />
                        <span style={{ color: 'var(--ink-500)', fontWeight: 500, marginLeft: 6, fontSize: 11 }}>
                          {TIER_LABEL[c.tier]} · {c.mbti}
                        </span>
                      </div>
                    </div>
                    <button
                      onClick={() => onRemove?.(c.id)}
                      aria-label="제외"
                      style={{
                        width: 24, height: 24, borderRadius: 6,
                        background: 'var(--bg-soft)', color: 'var(--ink-500)',
                        fontSize: 14, fontWeight: 700, flexShrink: 0,
                      }}
                    >×</button>
                  </div>
                ))}
                {candidates.length === 0 && (
                  <div style={{ color: 'var(--ink-400)', fontSize: 12, padding: '10px 4px' }}>
                    선택된 후보가 없습니다. 모달을 닫고 다시 선택해 주세요.
                  </div>
                )}
              </div>
            </div>
          </div>
        </div>

        {/* 푸터 — 복사 / 제안 등록 / 닫기 */}
        <div style={{
          padding: '14px 22px 18px', borderTop: '1px solid var(--line)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          gap: 12, flexWrap: 'wrap',
        }}>
          <div style={{
            fontSize: 12,
            color: proposed ? '#14855a' : copied ? '#14855a' : 'var(--ink-500)',
            fontWeight: (proposed || copied) ? 700 : 500,
            flex: '1 1 200px', minWidth: 0,
          }}>
            {proposed
              ? '✓ 매칭 기록에 등록 완료 — 회원 응답 후 "최종 선택" 처리하세요'
              : copied
              ? '✓ 카톡 멘트가 복사됐어요. 매칭 기록에도 등록하려면 우측 버튼을 눌러주세요'
              : '① 멘트 복사 → 카톡 전송 ② 제안 등록 → 매칭 기록에 트리로 남음'}
          </div>
          <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
            <button className="btn ghost sm" onClick={onClose}>닫기</button>
            <button
              className="btn ghost sm"
              onClick={copy}
              disabled={candidates.length === 0}
              style={{
                opacity: candidates.length === 0 ? 0.5 : 1,
                cursor: candidates.length === 0 ? 'not-allowed' : 'pointer',
                padding: '0 14px',
              }}
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ marginRight: 2 }}>
                <rect x="8" y="8" width="12" height="12" rx="2" stroke="currentColor" strokeWidth="2"/>
                <path d="M4 16V6a2 2 0 012-2h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
              </svg>
              {copied ? '복사됨' : '카톡 멘트 복사'}
            </button>
            <button
              className="btn brand sm"
              onClick={async () => {
                if (proposing || proposed) return;
                if (!onPropose) return;
                setProposing(true);
                const r = await onPropose(candidates.map(c => c.id));
                setProposing(false);
                if (r && r.ok) setProposed(true);
              }}
              disabled={candidates.length === 0 || proposing || proposed}
              style={{
                opacity: candidates.length === 0 || proposing || proposed ? 0.6 : 1,
                cursor: candidates.length === 0 || proposing || proposed ? 'not-allowed' : 'pointer',
                padding: '0 18px',
                background: proposed ? '#14855a' : undefined,
              }}
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ marginRight: 4 }}>
                <path d="M5 12l5 5L20 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              {proposed ? '등록됨' : proposing ? '등록 중…' : `이 제안 등록 (${candidates.length}명)`}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function RelayToggle({ label, options, value, onChange }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
      <span style={{ fontSize: 13, color: 'var(--ink-700)' }}>{label}</span>
      <div style={{ display: 'inline-flex', background: 'var(--bg-soft)', borderRadius: 8, padding: 3 }}>
        {options.map(o => (
          <button
            key={o.v}
            onClick={() => onChange(o.v)}
            style={{
              padding: '6px 10px', borderRadius: 6,
              fontSize: 12, fontWeight: 600,
              background: value === o.v ? '#fff' : 'transparent',
              color: value === o.v ? 'var(--ink-900)' : 'var(--ink-500)',
              boxShadow: value === o.v ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
            }}
          >{o.l}</button>
        ))}
      </div>
    </div>
  );
}

function RelayCheckRow({ label, checked, onToggle }) {
  return (
    <button
      onClick={onToggle}
      style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10,
        padding: '8px 10px', borderRadius: 10,
        background: 'transparent', border: '1px solid var(--line)',
        textAlign: 'left',
      }}
    >
      <span style={{ fontSize: 13, color: 'var(--ink-700)' }}>{label}</span>
      <span style={{
        width: 18, height: 18, borderRadius: 5,
        border: `1.5px solid ${checked ? 'var(--ink-900)' : 'var(--ink-300)'}`,
        background: checked ? 'var(--ink-900)' : '#fff',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        color: '#fff', fontSize: 10, fontWeight: 700, flexShrink: 0,
      }}>{checked ? '✓' : ''}</span>
    </button>
  );
}

// ═════════════════════════════════════════════════════════════
// 매칭 진행 · 회원 선택 (회원 상세를 거치지 않고 바로 진입한 경우)
// ═════════════════════════════════════════════════════════════
function AdminMatchPick({ go, members, startMatch, matches, seenNewMemberIds = [], openDetail }) {
  const [gender, setGender] = React.useState('all');
  const [q, setQ] = React.useState('');
  const matchedIds = new Set((matches || []).flatMap(m => [m.a, m.b]));

  const list = members
    .filter(m => gender === 'all' || m.gender === gender)
    .filter(m => !q || m.name.toLowerCase().includes(q.toLowerCase()))
    .sort((a, b) => a.daysAgo - b.daysAgo);

  return (
    <AdminShell go={go} active="match" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">매칭 진행 · 회원 선택</div>
          <h1>누구의 매칭을 진행할까요?</h1>
        </div>
      </div>

      <div style={{
        background: '#fff7ec', border: '1px solid #f3e3b8', borderRadius: 16,
        padding: '14px 18px', marginBottom: 16, fontSize: 13.5, color: '#8c6a05',
        lineHeight: 1.6,
      }}>
        💡 매칭은 <b>본인 1명을 기준</b>으로 반대 성별 후보를 호환도 기반으로 추천합니다.<br />
        먼저 매칭을 진행할 회원을 선택하세요.
      </div>

      <div className="filter-bar">
        <div className="search">
          <span style={{ color: 'var(--ink-400)' }}>⌕</span>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="이름으로 빠르게 찾기" />
        </div>
        <div className="seg">
          {[{ v: 'all', l: '전체' }, { v: 'M', l: '남자' }, { v: 'F', l: '여자' }].map(g => (
            <button key={g.v} className={gender === g.v ? 'on' : ''} onClick={() => setGender(g.v)}>{g.l}</button>
          ))}
        </div>
      </div>

      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 10,
      }}>
        {list.map(m => {
          const matchCount = (matches || []).filter(x => x.a === m.id || x.b === m.id).length;
          return (
            <button
              key={m.id}
              onClick={() => startMatch(m.id)}
              style={{
                background: '#fff', border: '1px solid var(--line)',
                borderRadius: 16, padding: 14, textAlign: 'left',
                display: 'flex', alignItems: 'center', gap: 12,
                transition: 'transform 100ms ease, box-shadow 100ms ease',
              }}
              onMouseEnter={e => { e.currentTarget.style.boxShadow = '0 6px 16px rgba(0,0,0,0.08)'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
              onMouseLeave={e => { e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.transform = 'translateY(0)'; }}
            >
              <div style={{
                width: 46, height: 46, borderRadius: 14,
                background: tierAvatarBg(m.tier), color: tierAvatarColor(m.tier),
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                fontSize: 18, fontWeight: 700, flexShrink: 0,
                border: m.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
              }}>{m.name.charAt(0)}</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
                  <MemberNameLink member={m} openDetail={openDetail} stopPropagation style={{ fontSize: 14.5, fontWeight: 700 }} />
                  <span style={{ fontSize: 12, color: 'var(--ink-500)' }}>{m.age} · {m.gender === 'M' ? '남' : '여'}</span>
                  {m.daysAgo <= 5 && !seenNewMemberIds.includes(m.id) && <span className="new-badge">NEW</span>}
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginTop: 5 }}>
                  <TierChip tier={m.tier} />
                  {matchCount > 0 && (
                    <span style={{
                      fontFamily: 'var(--f-mono)', fontSize: 10, fontWeight: 700,
                      background: 'var(--bg-soft)', color: 'var(--ink-700)',
                      padding: '2px 7px', borderRadius: 999,
                    }}>매칭 {matchCount}</span>
                  )}
                </div>
              </div>
              <div style={{ color: 'var(--ink-300)', fontSize: 18 }}>→</div>
            </button>
          );
        })}
      </div>
    </AdminShell>
  );
}

// ═════════════════════════════════════════════════════════════
// 설정 — 관리자 계정 + 매칭 문구 템플릿
// ═════════════════════════════════════════════════════════════

const DEFAULT_TEMPLATES = [
  {
    id: 'tpl-1',
    name: '기본 매칭 안내',
    body: `안녕하세요 {이름}님, Color-소개팅 운영자입니다 💌

매칭이 성사되어 안내드립니다.

▸ 상대: {상대이름} ({상대나이}, {상대성별})
▸ 등급: {상대등급}
▸ 지역: {상대지역}
▸ 직무: {상대직무}

상대 카카오톡 ID: {상대카톡}
인스타: {상대인스타}

먼저 인사 한번 보내주시고, 진행 결과는
오픈채팅 https://open.kakao.com/o/sEtOx7vi 로 알려주세요.

좋은 인연 되세요 :)`,
  },
  {
    id: 'tpl-2',
    name: '다이아·블랙 회원 안내',
    body: `안녕하세요 {이름}님, Color-소개팅입니다.

특별히 신중히 매칭한 {상대등급} 회원과의 만남이 잡혔습니다.

상대 프로필
▸ {상대이름} ({상대나이}, {상대성별}) · {상대MBTI}
▸ {상대직무} · {상대지역}

카톡 {상대카톡} · 인스타 {상대인스타}

격을 갖춘 톤으로 먼저 인사 부탁드립니다.`,
  },
  {
    id: 'tpl-3',
    name: '재매칭 안내',
    body: `{이름}님, Color-소개팅입니다.

이전 매칭이 잘 풀리지 않아 아쉬웠을 텐데,
다시 한 분 안내드립니다.

상대: {상대이름} ({상대나이}, {상대등급})

이번엔 잘 풀리길 응원할게요.`,
  },
];

const TEMPLATE_VARIABLES = [
  { v: '{이름}', d: '본인 이름' },
  { v: '{상대이름}', d: '상대 이름' },
  { v: '{상대나이}', d: '상대 나이' },
  { v: '{상대성별}', d: '상대 성별' },
  { v: '{상대등급}', d: '상대 색상 등급' },
  { v: '{상대지역}', d: '상대 거주지' },
  { v: '{상대직무}', d: '상대 직무' },
  { v: '{상대MBTI}', d: '상대 MBTI' },
  { v: '{상대카톡}', d: '상대 카카오톡 ID' },
  { v: '{상대인스타}', d: '상대 인스타 ID' },
  { v: '{상대연락처}', d: '상대 휴대폰 번호(숫자)' },
];

/** 설정 · 매칭 문구 미리보기용 회원 선택 */
function TplMemberSearch({ label, members, selectedId, onSelect }) {
  const [q, setQ] = React.useState('');
  const sel = members.find((m) => m.id === selectedId);
  const hits = React.useMemo(() => {
    const ql = q.trim().toLowerCase();
    let list = [...members];
    if (ql) {
      list = list.filter((m) =>
        (m.name || '').toLowerCase().includes(ql)
        || String(m.id || '').toLowerCase().includes(ql)
        || (m.region || '').toLowerCase().includes(ql)
        || (m.job || '').toLowerCase().includes(ql)
        || (m.mbti || '').toLowerCase().includes(ql),
      );
    }
    return list.slice(0, 14);
  }, [members, q]);

  return (
    <div style={{
      border: '1px solid var(--line)',
      borderRadius: 12,
      padding: '10px 12px',
      background: 'var(--bg-soft)',
      minWidth: 0,
    }}
    >
      <div className="tiny" style={{ marginBottom: 6 }}>{label}</div>
      {sel ? (
        <div style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          gap: 8,
          flexWrap: 'wrap',
          marginBottom: 8,
        }}
        >
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
            <TierChip tier={sel.tier} />
            <span style={{ fontWeight: 700 }}>{sel.name}</span>
            <span style={{ fontSize: 11, color: 'var(--ink-500)', fontFamily: 'var(--f-mono)' }}>{sel.id}</span>
          </div>
          <button type="button" className="btn ghost sm" onClick={() => onSelect(null)}>선택 해제</button>
        </div>
      ) : null}
      <input
        className="input"
        placeholder="이름 · 회원코드 · 지역 검색 후 아래에서 클릭"
        value={q}
        onChange={(e) => setQ(e.target.value)}
        style={{ fontSize: 13 }}
      />
      <div style={{
        marginTop: 6,
        maxHeight: 132,
        overflowY: 'auto',
        display: 'flex',
        flexDirection: 'column',
        gap: 2,
      }}
      >
        {hits.length === 0 ? (
          <div style={{ fontSize: 12, color: 'var(--ink-400)', padding: '8px 4px' }}>검색 결과가 없습니다.</div>
        ) : hits.map((m) => (
          <button
            key={m.id}
            type="button"
            onClick={() => { onSelect(m.id); setQ(''); }}
            style={{
              textAlign: 'left',
              padding: '8px 10px',
              borderRadius: 8,
              border: `1px solid ${m.id === selectedId ? 'var(--brand)' : 'transparent'}`,
              background: m.id === selectedId ? 'rgba(226,58,46,0.07)' : 'transparent',
              fontSize: 12.5,
              cursor: 'pointer',
            }}
          >
            <b>{m.name}</b>
            <span style={{ color: 'var(--ink-500)', marginLeft: 8 }}>{m.age}세 · {m.region || '—'}</span>
            <span style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-400)', marginLeft: 6 }}>{m.id}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function loadLS(key, fallback) {
  try {
    const v = localStorage.getItem(key);
    return v ? JSON.parse(v) : fallback;
  } catch { return fallback; }
}
function saveLS(key, value) {
  try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
}

function AdminSettings({ go, members, seenNewMemberIds = [] }) {
  // ── 관리자 계정 ──
  const [adminId, setAdminId] = React.useState('admin');
  const [adminIdInput, setAdminIdInput] = React.useState('admin');
  const [pwCur, setPwCur] = React.useState('');
  const [pwNew, setPwNew] = React.useState('');
  const [pwConfirm, setPwConfirm] = React.useState('');
  const [accountMsg, setAccountMsg] = React.useState(null);

  const [landApps, setLandApps] = React.useState('1284');
  const [landMatches, setLandMatches] = React.useState('342');
  const [landAvg, setLandAvg] = React.useState('4.7일');
  const [landMsg, setLandMsg] = React.useState(null);

  React.useEffect(() => {
    window.CSApi.getLandingDisplay().then((r) => {
      if (!r) return;
      setLandApps(String(r.cumulativeApplications ?? 1284));
      setLandMatches(String(r.successfulMatches ?? 342));
      setLandAvg(String(r.averageResponseLabel || '4.7일'));
    }).catch(() => {});
  }, []);

  const saveLandingDisplay = async () => {
    setLandMsg(null);
    try {
      const res = await window.CSApi.updateLandingDisplay({
        cumulativeApplications: parseInt(landApps, 10) || 0,
        successfulMatches: parseInt(landMatches, 10) || 0,
        averageResponseLabel: (landAvg || '').trim() || '4.7일',
      });
      setLandApps(String(res.cumulativeApplications));
      setLandMatches(String(res.successfulMatches));
      setLandAvg(String(res.averageResponseLabel || ''));
      setLandMsg({ type: 'ok', text: '사용자 메인에 반영되었습니다.' });
      setTimeout(() => setLandMsg(null), 2500);
    } catch (err) {
      setLandMsg({ type: 'err', text: err.message || '저장 실패' });
    }
  };

  React.useEffect(() => {
    window.CSApi.me().then(r => {
      if (r?.admin?.loginId) {
        setAdminId(r.admin.loginId);
        setAdminIdInput(r.admin.loginId);
      }
    }).catch(() => {});
  }, []);

  const saveAccount = async () => {
    if (!adminIdInput.trim()) { setAccountMsg({ type: 'err', text: '아이디를 입력해주세요.' }); return; }
    if (pwNew || pwConfirm || pwCur) {
      if (!pwCur) { setAccountMsg({ type: 'err', text: '현재 비밀번호를 입력해주세요.' }); return; }
      if (pwNew.length < 6) { setAccountMsg({ type: 'err', text: '새 비밀번호는 6자 이상이어야 합니다.' }); return; }
      if (pwNew !== pwConfirm) { setAccountMsg({ type: 'err', text: '새 비밀번호 확인이 일치하지 않습니다.' }); return; }
    }
    try {
      const res = await window.CSApi.changeAccount({
        loginId: adminIdInput,
        currentPassword: pwCur || undefined,
        newPassword: pwNew || undefined,
        confirmPassword: pwConfirm || undefined,
      });
      setAdminId(res.admin.loginId);
      setPwCur(''); setPwNew(''); setPwConfirm('');
      setAccountMsg({ type: 'ok', text: `저장되었어요. (아이디: ${res.admin.loginId}${res.passwordChanged ? ' · 비밀번호 변경됨' : ''})` });
      setTimeout(() => setAccountMsg(null), 3000);
    } catch (err) {
      setAccountMsg({ type: 'err', text: err.message || '저장에 실패했습니다.' });
    }
  };

  // ── 매칭 문구 템플릿 (서버 동기화) ──
  const [templates, setTemplates] = React.useState([]);
  const [activeTplId, setActiveTplId] = React.useState(null);

  React.useEffect(() => {
    window.CSApi.listTemplates().then(r => {
      const list = (r?.templates || []).map(t => ({ id: String(t._id), name: t.name, body: t.body }));
      setTemplates(list);
      if (list.length > 0 && !activeTplId) setActiveTplId(list[0].id);
    }).catch(() => {});
  }, []);
  const [draftName, setDraftName] = React.useState('');
  const [draftBody, setDraftBody] = React.useState('');
  const [dirty, setDirty] = React.useState(false);
  const [previewOn, setPreviewOn] = React.useState(false);
  const [tplMsg, setTplMsg] = React.useState(null);
  const bodyRef = React.useRef(null);
  const [tplSelfId, setTplSelfId] = React.useState(null);
  const [tplPeerId, setTplPeerId] = React.useState(null);
  const [tplWide, setTplWide] = React.useState(() => typeof window !== 'undefined' && window.innerWidth >= 1024);

  React.useEffect(() => {
    const mq = window.matchMedia('(min-width: 1024px)');
    const fn = () => setTplWide(mq.matches);
    fn();
    mq.addEventListener('change', fn);
    return () => mq.removeEventListener('change', fn);
  }, []);

  // sync draft when template changes
  React.useEffect(() => {
    const t = templates.find(x => x.id === activeTplId);
    if (t) {
      setDraftName(t.name);
      setDraftBody(t.body);
      setDirty(false);
    }
  }, [activeTplId]);

  const activeTpl = templates.find(t => t.id === activeTplId);

  const persistTemplates = (next) => {
    setTemplates(next);
  };

  const saveTpl = async () => {
    if (!draftName.trim()) { setTplMsg({ type: 'err', text: '템플릿 이름을 입력해주세요.' }); return; }
    try {
      const res = await window.CSApi.updateTemplate(activeTplId, draftName, draftBody);
      const next = templates.map(t => t.id === activeTplId ? { id: String(res.template._id), name: res.template.name, body: res.template.body } : t);
      persistTemplates(next);
      setDirty(false);
      setTplMsg({ type: 'ok', text: '저장되었어요.' });
      setTimeout(() => setTplMsg(null), 2400);
    } catch (err) {
      setTplMsg({ type: 'err', text: err.message || '저장 실패' });
    }
  };

  const newTpl = async () => {
    try {
      const res = await window.CSApi.createTemplate('새 템플릿', '안녕하세요 {이름}님,\n\n');
      const created = { id: String(res.template._id), name: res.template.name, body: res.template.body };
      persistTemplates([created, ...templates]);
      setActiveTplId(created.id);
      setTplMsg({ type: 'ok', text: '새 템플릿이 생성됐어요.' });
      setTimeout(() => setTplMsg(null), 2400);
    } catch (err) {
      setTplMsg({ type: 'err', text: err.message || '생성 실패' });
    }
  };

  const dupTpl = async () => {
    if (!activeTpl) return;
    try {
      const res = await window.CSApi.createTemplate(activeTpl.name + ' (복사)', activeTpl.body);
      const created = { id: String(res.template._id), name: res.template.name, body: res.template.body };
      persistTemplates([created, ...templates]);
      setActiveTplId(created.id);
    } catch (err) {
      setTplMsg({ type: 'err', text: err.message || '복제 실패' });
    }
  };

  const deleteTpl = async () => {
    if (!activeTpl) return;
    if (templates.length === 1) { setTplMsg({ type: 'err', text: '최소 1개의 템플릿은 남겨두세요.' }); return; }
    if (!confirm(`"${activeTpl.name}" 템플릿을 삭제할까요?`)) return;
    try {
      await window.CSApi.deleteTemplate(activeTplId);
      const next = templates.filter(t => t.id !== activeTplId);
      persistTemplates(next);
      setActiveTplId(next[0]?.id);
    } catch (err) {
      setTplMsg({ type: 'err', text: err.message || '삭제 실패' });
    }
  };

  const resetTpls = () => {
    setTplMsg({ type: 'err', text: '기본값 되돌리기는 비활성화되어 있습니다. (DB 직접 관리)' });
    setTimeout(() => setTplMsg(null), 2400);
  };

  const insertVar = (v) => {
    const ta = bodyRef.current;
    if (!ta) return;
    const start = ta.selectionStart, end = ta.selectionEnd;
    const next = draftBody.slice(0, start) + v + draftBody.slice(end);
    setDraftBody(next);
    setDirty(true);
    setTimeout(() => {
      ta.focus();
      ta.selectionStart = ta.selectionEnd = start + v.length;
    }, 0);
  };

  const previewMe = members.find((m) => m.id === tplSelfId) || members.find((m) => m.gender === 'M') || members[0];
  const previewPeer = members.find((m) => m.id === tplPeerId) || members.find((m) => m.gender === 'F') || members[0];

  const renderPreview = (body) => {
    const me = previewMe;
    const sample = previewPeer;
    const peerJob = sample?.role
      || (sample?.job ? String(sample.job).split(/·/)[0].trim() : '')
      || '—';
    const peerRegion = (sample?.region || '').split(/\s+/).filter(Boolean).slice(0, 2).join(' ') || '—';
    const peerPhone = String(sample?.contact || '').trim();
    const peerContact = peerPhone || '—';
    return body
      .replace(/{이름}/g, me?.name || '홍길동')
      .replace(/{상대이름}/g, sample?.name || '김지영')
      .replace(/{상대나이}/g, sample?.age != null ? String(sample.age) : '28')
      .replace(/{상대성별}/g, sample ? (sample.gender === 'M' ? '남' : '여') : '여')
      .replace(/{상대등급}/g, sample?.tier ? TIER_LABEL[sample.tier] : TIER_LABEL.blue)
      .replace(/{상대지역}/g, peerRegion)
      .replace(/{상대직무}/g, peerJob)
      .replace(/{상대MBTI}/g, sample?.mbti || 'ENFP')
      .replace(/{상대연락처}/g, peerContact)
      .replace(/{상대카톡}/g, sample?.kakao || 'sample_kakao')
      .replace(/{상대인스타}/g, sample?.insta || '@sample');
  };

  return (
    <AdminShell go={go} active="settings" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">운영 설정</div>
          <h1>설정</h1>
        </div>
      </div>

      <div className="settings-grid">
        {/* ── 사용자 메인 노출 수치 ── */}
        <div className="adm-card" style={{ padding: 22 }}>
          <div className="tiny">사용자 화면</div>
          <div className="chart-title" style={{ marginTop: 4, marginBottom: 8 }}>메인 홈 통계 배너</div>
          <div style={{ fontSize: 12, color: 'var(--ink-500)', marginBottom: 16, lineHeight: 1.5 }}>
            접수 메인 상단에 보이는 <b>누적 신청 · 성사 매칭 · 평균 응답</b> 수치입니다.{' '}
            <b>누적 신청</b>은 신규 접수가 완료될 때마다, <b>성사 매칭</b>은 매칭 트리에서「최종 선택」버튼을 눌렀을 때마다 각각 서버에서 1씩 자동 증가합니다.
            필요 시 아래에서 수동으로 수정한 뒤 저장해 보정할 수 있으며, 저장 즉시 사용자 화면에 반영됩니다(새로고침 또는 최대 1분).
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
            <SettingField label="누적 신청" hint="정수">
              <input
                className="input"
                type="number"
                min={0}
                value={landApps}
                onChange={e => setLandApps(e.target.value.replace(/[^\d]/g, ''))}
              />
            </SettingField>
            <SettingField label="성사 매칭" hint="정수">
              <input
                className="input"
                type="number"
                min={0}
                value={landMatches}
                onChange={e => setLandMatches(e.target.value.replace(/[^\d]/g, ''))}
              />
            </SettingField>
            <SettingField label="평균 응답" hint="표시 문자열">
              <input
                className="input"
                value={landAvg}
                onChange={e => setLandAvg(e.target.value)}
                placeholder="예: 4.7일, 4.7d"
              />
            </SettingField>
          </div>
          {landMsg && (
            <div style={{
              marginTop: 12, padding: '10px 14px', borderRadius: 10,
              background: landMsg.type === 'ok' ? '#e8f7ee' : '#fee5e2',
              color: landMsg.type === 'ok' ? '#14855a' : '#a8281d',
              fontSize: 13, fontWeight: 500,
            }}>
              {landMsg.type === 'ok' ? '✓ ' : '⚠️ '}{landMsg.text}
            </div>
          )}
          <div style={{ marginTop: 14 }}>
            <button type="button" className="btn primary sm" onClick={saveLandingDisplay}>메인 수치 저장</button>
          </div>
        </div>

        {/* ── 관리자 계정 ── */}
        <div className="adm-card" style={{ padding: 22 }}>
          <div className="tiny">관리자 계정</div>
          <div className="chart-title" style={{ marginTop: 4, marginBottom: 18 }}>로그인 정보 변경</div>

          <SettingField label="관리자 아이디" hint="콘솔 로그인 시 사용">
            <input
              className="input"
              value={adminIdInput}
              onChange={e => setAdminIdInput(e.target.value)}
              placeholder="영문·숫자 등 로그인 아이디"
            />
          </SettingField>

          <div style={{
            margin: '18px 0 14px', padding: '10px 12px',
            background: 'var(--bg-soft)', borderRadius: 10,
            fontSize: 12, color: 'var(--ink-500)',
          }}>
            🔒 비밀번호 변경하지 않으려면 비워두세요
          </div>

          <SettingField label="현재 비밀번호">
            <input className="input" type="password" value={pwCur} onChange={e => setPwCur(e.target.value)} placeholder="••••••••" />
          </SettingField>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            <SettingField label="새 비밀번호" hint="6자 이상">
              <input className="input" type="password" value={pwNew} onChange={e => setPwNew(e.target.value)} placeholder="••••••••" />
            </SettingField>
            <SettingField label="새 비밀번호 확인">
              <input className="input" type="password" value={pwConfirm} onChange={e => setPwConfirm(e.target.value)} placeholder="••••••••" />
            </SettingField>
          </div>

          {accountMsg && (
            <div style={{
              marginTop: 12, padding: '10px 14px', borderRadius: 10,
              background: accountMsg.type === 'ok' ? '#e8f7ee' : '#fee5e2',
              color: accountMsg.type === 'ok' ? '#14855a' : '#a8281d',
              fontSize: 13, fontWeight: 500,
            }}>
              {accountMsg.type === 'ok' ? '✓ ' : '⚠️ '}{accountMsg.text}
            </div>
          )}

          <div style={{ display: 'flex', gap: 8, marginTop: 18 }}>
            <button className="btn primary" onClick={saveAccount}>변경사항 저장</button>
            <button
              className="btn ghost"
              onClick={() => {
                setAdminIdInput(adminId); setPwCur(''); setPwNew(''); setPwConfirm('');
              }}
            >취소</button>
          </div>

          <div style={{
            marginTop: 22, padding: '14px 16px',
            background: '#161412', color: '#fff', borderRadius: 12,
          }}>
            <div className="tiny" style={{ color: 'rgba(255,255,255,0.5)' }}>마지막 로그인</div>
            <div style={{ marginTop: 6, fontSize: 13, fontFamily: 'var(--f-mono)' }}>
              26.05.22 (목) 09:14 · 121.142.xxx.xxx
            </div>
            <div style={{ marginTop: 2, fontSize: 11, color: 'rgba(255,255,255,0.4)', fontFamily: 'var(--f-mono)' }}>
              이전: 26.05.21 (수) 22:48 · Chrome / macOS
            </div>
          </div>
        </div>

        {/* ── 매칭 문구 템플릿 (전체 너비 · 회원 선택 미리보기) ── */}
        <div className="adm-card tpl-card-full" style={{ padding: 22 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14, flexWrap: 'wrap', gap: 10 }}>
            <div>
              <div className="tiny">매칭 안내 문구</div>
              <div className="chart-title" style={{ marginTop: 4 }}>저장된 템플릿 {templates.length}개</div>
              <div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 6, maxWidth: 720, lineHeight: 1.5 }}>
                아래에서 <b>수신 회원</b>·<b>상대 회원</b>을 검색해 선택하면 미리보기에 바로 반영됩니다. 선택하지 않으면 예시(목록에 있는 남·여 회원 순)로 표시합니다.
              </div>
            </div>
            <div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
              <button type="button" className="btn ghost sm" onClick={resetTpls}>기본값으로</button>
              <button type="button" className="btn primary sm" onClick={newTpl}>+ 새 템플릿</button>
            </div>
          </div>

          <div className="tpl-layout">
            <aside className="tpl-aside">
              <div className="tiny" style={{ marginBottom: 8 }}>템플릿</div>
              <div className="tpl-aside-list">
                {templates.map((t) => (
                  <button
                    key={t.id}
                    type="button"
                    onClick={() => {
                      if (dirty && !confirm('저장하지 않은 변경사항이 있어요. 그래도 이동할까요?')) return;
                      setActiveTplId(t.id);
                    }}
                    style={{
                      padding: '10px 12px',
                      borderRadius: 10,
                      textAlign: 'left',
                      background: t.id === activeTplId ? 'var(--ink-900)' : 'transparent',
                      color: t.id === activeTplId ? '#fff' : 'var(--ink-700)',
                      border: t.id === activeTplId ? '0' : '1px solid var(--line)',
                      transition: 'all 120ms ease',
                    }}
                  >
                    <div style={{ fontSize: 13, fontWeight: 600, letterSpacing: '-0.005em' }}>{t.name}</div>
                    <div style={{
                      fontSize: 11,
                      marginTop: 3,
                      color: t.id === activeTplId ? 'rgba(255,255,255,0.5)' : 'var(--ink-400)',
                      overflow: 'hidden',
                      textOverflow: 'ellipsis',
                      whiteSpace: 'nowrap',
                    }}
                    >{(t.body || '').replace(/\n/g, ' ').slice(0, 56)}{(t.body || '').length > 56 ? '…' : ''}</div>
                  </button>
                ))}
              </div>
            </aside>

            <div className="tpl-main" style={{ minWidth: 0 }}>
              {activeTpl ? (
                <>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}>
                    <input
                      className="input"
                      value={draftName}
                      onChange={(e) => { setDraftName(e.target.value); setDirty(true); }}
                      style={{ flex: '1 1 220px', fontSize: 15, fontWeight: 600, minWidth: 0 }}
                      placeholder="템플릿 이름"
                    />
                    {!tplWide && (
                      <div style={{ display: 'inline-flex', background: 'var(--bg-soft)', borderRadius: 10, padding: 3 }}>
                        <button
                          type="button"
                          onClick={() => setPreviewOn(false)}
                          style={{
                            padding: '7px 12px',
                            borderRadius: 8,
                            fontSize: 12,
                            fontWeight: 600,
                            background: !previewOn ? '#fff' : 'transparent',
                            color: !previewOn ? 'var(--ink-900)' : 'var(--ink-500)',
                            boxShadow: !previewOn ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
                          }}
                        >편집</button>
                        <button
                          type="button"
                          onClick={() => setPreviewOn(true)}
                          style={{
                            padding: '7px 12px',
                            borderRadius: 8,
                            fontSize: 12,
                            fontWeight: 600,
                            background: previewOn ? '#fff' : 'transparent',
                            color: previewOn ? 'var(--ink-900)' : 'var(--ink-500)',
                            boxShadow: previewOn ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
                          }}
                        >미리보기</button>
                      </div>
                    )}
                  </div>

                  <div className="tpl-pickers">
                    <TplMemberSearch
                      label="미리보기 · 수신 회원 → {이름}"
                      members={members}
                      selectedId={tplSelfId}
                      onSelect={setTplSelfId}
                    />
                    <TplMemberSearch
                      label="미리보기 · 상대 회원 → {상대이름} 등"
                      members={members}
                      selectedId={tplPeerId}
                      onSelect={setTplPeerId}
                    />
                  </div>

                  {tplWide ? (
                    <div className="tpl-split">
                      <div className="tpl-edit-col">
                        <textarea
                          ref={bodyRef}
                          className="textarea"
                          value={draftBody}
                          onChange={(e) => { setDraftBody(e.target.value); setDirty(true); }}
                          placeholder="안내 문구를 입력하세요…"
                          style={{ minHeight: 240, fontFamily: 'var(--f-sans)', fontSize: 13.5, lineHeight: 1.65, width: '100%', boxSizing: 'border-box' }}
                        />
                        <div style={{ marginTop: 10 }}>
                          <div className="tiny" style={{ marginBottom: 6 }}>변수 · 클릭해 삽입</div>
                          <div className="tpl-vars-wrap">
                            {TEMPLATE_VARIABLES.map((v) => (
                              <button
                                key={v.v}
                                type="button"
                                onClick={() => insertVar(v.v)}
                                title={v.d}
                                className="tpl-var-btn"
                              >{v.v}</button>
                            ))}
                          </div>
                        </div>
                      </div>
                      <div className="tpl-preview-pane">
                        {renderPreview(draftBody)}
                        <div className="tpl-preview-hint">
                          선택한 회원 기준 치환 · 실제 발송 시에는 매칭 화면에서 지정한 상대 정보로 대체됩니다.
                        </div>
                      </div>
                    </div>
                  ) : (
                    <>
                      {!previewOn ? (
                        <>
                          <textarea
                            ref={bodyRef}
                            className="textarea"
                            value={draftBody}
                            onChange={(e) => { setDraftBody(e.target.value); setDirty(true); }}
                            placeholder="안내 문구를 입력하세요…"
                            style={{ minHeight: 260, fontFamily: 'var(--f-sans)', fontSize: 13.5, lineHeight: 1.65, width: '100%', boxSizing: 'border-box' }}
                          />
                          <div style={{ marginTop: 10 }}>
                            <div className="tiny" style={{ marginBottom: 6 }}>변수 · 클릭해 삽입</div>
                            <div className="tpl-vars-wrap">
                              {TEMPLATE_VARIABLES.map((v) => (
                                <button
                                  key={v.v}
                                  type="button"
                                  onClick={() => insertVar(v.v)}
                                  title={v.d}
                                  className="tpl-var-btn"
                                >{v.v}</button>
                              ))}
                            </div>
                          </div>
                        </>
                      ) : (
                        <div className="tpl-preview-pane tpl-preview-pane--solo">
                          {renderPreview(draftBody)}
                          <div className="tpl-preview-hint">
                            선택한 회원 기준 치환 · 실제 발송 시에는 매칭 화면에서 지정한 상대 정보로 대체됩니다.
                          </div>
                        </div>
                      )}
                    </>
                  )}

                  {tplMsg && (
                    <div style={{
                      marginTop: 12,
                      padding: '10px 14px',
                      borderRadius: 10,
                      background: tplMsg.type === 'ok' ? '#e8f7ee' : '#fee5e2',
                      color: tplMsg.type === 'ok' ? '#14855a' : '#a8281d',
                      fontSize: 13,
                      fontWeight: 500,
                    }}
                    >
                      {tplMsg.type === 'ok' ? '✓ ' : '⚠️ '}{tplMsg.text}
                    </div>
                  )}

                  <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 14, gap: 8, flexWrap: 'wrap' }}>
                    <div style={{ display: 'flex', gap: 6 }}>
                      <button type="button" className="btn ghost sm" onClick={dupTpl}>복제</button>
                      <button type="button" className="btn ghost sm" onClick={deleteTpl} style={{ color: 'var(--brand-ink)' }}>삭제</button>
                    </div>
                    <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
                      {dirty && (
                        <span style={{ fontSize: 11, color: 'var(--brand-ink)', fontFamily: 'var(--f-mono)', letterSpacing: '0.04em' }}>
                          ● 미저장
                        </span>
                      )}
                      <button type="button" className="btn primary sm" onClick={saveTpl} disabled={!dirty} style={{ opacity: dirty ? 1 : 0.5 }}>저장</button>
                    </div>
                  </div>
                </>
              ) : (
                <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-400)', fontSize: 14 }}>
                  왼쪽에서 템플릿을 선택하거나 새로 만드세요.
                </div>
              )}
            </div>
          </div>
        </div>
      </div>
    </AdminShell>
  );
}

function SettingField({ label, hint, children }) {
  return (
    <div style={{ marginBottom: 14 }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
        marginBottom: 6, fontSize: 13, color: 'var(--ink-700)', fontWeight: 600,
      }}>
        <span>{label}</span>
        {hint && <span style={{ fontSize: 11, color: 'var(--ink-400)', fontFamily: 'var(--f-mono)', fontWeight: 500 }}>{hint}</span>}
      </div>
      {children}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// 활동 로그 — 회원 매칭 통계 + 결제 내역
// ═════════════════════════════════════════════════════════════

function AdminActivityLog({ go, members, openActivity, openDetail, seenNewMemberIds = [] }) {
  const [gender, setGender] = React.useState('all');
  const [tier, setTier] = React.useState('all');
  const [sort, setSort] = React.useState('try'); // try / success / paid / recent
  const [q, setQ] = React.useState('');

  const filtered = React.useMemo(() => {
    let list = members.slice();
    if (gender !== 'all') list = list.filter(m => m.gender === gender);
    if (tier !== 'all') list = list.filter(m => m.tier === tier);
    if (q) {
      const ql = q.toLowerCase();
      list = list.filter(m =>
        m.name.toLowerCase().includes(ql) ||
        m.job.toLowerCase().includes(ql) ||
        m.region.toLowerCase().includes(ql) ||
        (m.workplace || '').toLowerCase().includes(ql) ||
        m.id.toLowerCase().includes(ql)
      );
    }
    if (sort === 'try') list.sort((a, b) => (b.matchTry || 0) - (a.matchTry || 0));
    else if (sort === 'success') list.sort((a, b) => (b.matchSuccess || 0) - (a.matchSuccess || 0));
    else if (sort === 'paid') list.sort((a, b) => (b.totalPaid || 0) - (a.totalPaid || 0));
    else list.sort((a, b) => a.daysAgo - b.daysAgo);
    return list;
  }, [members, gender, tier, q, sort]);

  // Totals
  const totals = members.reduce((acc, m) => ({
    try: acc.try + (m.matchTry || 0),
    success: acc.success + (m.matchSuccess || 0),
    fail: acc.fail + (m.matchFail || 0),
    paid: acc.paid + (m.totalPaid || 0),
  }), { try: 0, success: 0, fail: 0, paid: 0 });
  const successRate = totals.try > 0 ? Math.round((totals.success / totals.try) * 100) : 0;

  return (
    <AdminShell go={go} active="log" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">회원별 활동 로그</div>
          <h1>활동 로그</h1>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button
            type="button"
            className="btn ghost sm"
            onClick={() => window.downloadMembersCsv?.(filtered, 'activity-log')}
          >목록 저장 (CSV)</button>
        </div>
      </div>

      {/* Top KPI strip */}
      <div className="adm-grid-4" style={{ marginBottom: 16 }}>
        <KPI label="총 매칭 시도" value={totals.try.toLocaleString()} unit="회" />
        <KPI label="성공 매칭" value={totals.success.toLocaleString()} unit="건" sub={`성공률 ${successRate}%`} subClass="up" accent="#14855a" />
        <KPI label="실패 매칭" value={totals.fail.toLocaleString()} unit="건" accent="#ec4438" />
        <KPI label="총 결제 금액" value={`₩ ${totals.paid.toLocaleString()}`} sub="유료 베타 누적" subClass="brand" dark />
      </div>

      {/* Filter bar */}
      <div className="filter-bar">
        <div className="search">
          <span style={{ color: 'var(--ink-400)' }}>⌕</span>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="이름·직업·ID 검색" />
        </div>
        <div className="seg">
          {[{ v: 'all', l: '전체' }, { v: 'M', l: '남자' }, { v: 'F', l: '여자' }].map(g => (
            <button key={g.v} className={gender === g.v ? 'on' : ''} onClick={() => setGender(g.v)}>{g.l}</button>
          ))}
        </div>
        <div className="tier-pills">
          {[
            { v: 'all', l: '모든 등급', dot: null },
            { v: 'diamond', l: 'D', dot: 'linear-gradient(135deg, #9adfff, #c8b8ff, #ffc6f0)' },
            { v: 'black', l: 'B', dot: '#161412' },
            { v: 'red', l: 'R', dot: '#e23a2e' },
            { v: 'blue', l: 'L', dot: '#2f6cf6' },
            { v: 'yellow', l: 'Y', dot: '#f5c419' },
            { v: 'white', l: 'W', dot: '#fff' },
          ].map(t => (
            <button key={t.v} className={`tier-pill ${tier === t.v ? 'on' : ''}`} onClick={() => setTier(t.v)}>
              {t.dot && <span className="dot" style={{ background: t.dot, borderColor: t.dot === '#fff' ? 'var(--ink-300)' : 'rgba(0,0,0,0.1)' }} />}
              {t.l}
            </button>
          ))}
        </div>
        <div className="seg">
          {[
            { v: 'try', l: '시도순' },
            { v: 'success', l: '성공순' },
            { v: 'paid', l: '결제순' },
            { v: 'recent', l: '최신순' },
          ].map(s => (
            <button key={s.v} className={sort === s.v ? 'on' : ''} onClick={() => setSort(s.v)}>↕ {s.l}</button>
          ))}
        </div>
      </div>

      {/* Table */}
      <div className="tbl-wrap">
        <table className="tbl">
          <thead>
            <tr>
              <th style={{ width: '20%' }}>회원</th>
              <th>등급</th>
              <th>매칭 시도</th>
              <th>성공</th>
              <th>실패</th>
              <th>진행중</th>
              <th>성공률</th>
              <th>총 결제</th>
              <th>마지막 활동</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {filtered.map(m => {
              const rate = m.matchTry > 0 ? Math.round((m.matchSuccess / m.matchTry) * 100) : 0;
              return (
                <tr key={m.id} onClick={() => openActivity(m.id)}>
                  <td>
                    <div className="col-name">
                      <div className="av" style={{
                        background: tierAvatarBg(m.tier), color: tierAvatarColor(m.tier),
                        border: m.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
                      }}>{m.name.charAt(0)}</div>
                      <div>
                        <div className="av-name">
                          <MemberNameLink member={m} openDetail={openDetail} stopPropagation />
                          <span style={{ fontSize: 12, color: 'var(--ink-500)', fontWeight: 500, marginLeft: 4 }}>{m.age}{m.gender === 'M' ? '남' : '여'}</span>
                        </div>
                        <div className="meta" style={{ fontFamily: 'var(--f-mono)' }}>{m.id}</div>
                      </div>
                    </div>
                  </td>
                  <td><TierChip tier={m.tier} /></td>
                  <td style={{ fontFamily: 'var(--f-mono)', fontSize: 14, fontWeight: 600 }}>{m.matchTry || 0}</td>
                  <td style={{ fontFamily: 'var(--f-mono)', fontSize: 14, fontWeight: 600, color: '#14855a' }}>{m.matchSuccess || 0}</td>
                  <td style={{ fontFamily: 'var(--f-mono)', fontSize: 14, fontWeight: 600, color: '#a8281d' }}>{m.matchFail || 0}</td>
                  <td style={{ fontFamily: 'var(--f-mono)', fontSize: 14, fontWeight: 600, color: '#2f6cf6' }}>{m.matchOngoing || 0}</td>
                  <td>
                    {m.matchTry > 0 ? (
                      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                        <div style={{ width: 50, height: 5, background: 'var(--bg-soft)', borderRadius: 999, overflow: 'hidden' }}>
                          <div style={{
                            height: '100%', width: `${rate}%`,
                            background: rate >= 50 ? '#14855a' : rate >= 25 ? '#f5c419' : '#ec4438',
                          }} />
                        </div>
                        <span style={{ fontSize: 12, fontFamily: 'var(--f-mono)', color: 'var(--ink-700)', fontWeight: 600 }}>{rate}%</span>
                      </div>
                    ) : <span style={{ color: 'var(--ink-400)', fontSize: 12 }}>-</span>}
                  </td>
                  <td style={{ fontFamily: 'var(--f-mono)', fontSize: 13, fontWeight: 600 }}>
                    {m.totalPaid > 0 ? `₩${(m.totalPaid / 1000).toFixed(0)}K` : <span style={{ color: 'var(--ink-400)' }}>-</span>}
                  </td>
                  <td style={{ color: 'var(--ink-500)', fontFamily: 'var(--f-mono)', fontSize: 12 }}>{m.daysAgo}일 전</td>
                  <td style={{ color: 'var(--ink-300)', textAlign: 'right' }}>›</td>
                </tr>
              );
            })}
          </tbody>
        </table>

        <div className="adm-mobile-list">
          {filtered.map(m => {
            const rate = m.matchTry > 0 ? Math.round((m.matchSuccess / m.matchTry) * 100) : 0;
            return (
              <button key={m.id} onClick={() => openActivity(m.id)} style={{
                width: '100%', display: 'flex', alignItems: 'center', gap: 12,
                background: '#fff', border: '1px solid var(--line)',
                borderRadius: 16, padding: 14, textAlign: 'left',
              }}>
                <div className="av" style={{
                  width: 48, height: 48, borderRadius: 14,
                  background: tierAvatarBg(m.tier), color: tierAvatarColor(m.tier),
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 18, fontWeight: 700, flexShrink: 0,
                  border: m.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
                }}>{m.name.charAt(0)}</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
                    <span style={{ fontSize: 14.5, fontWeight: 700 }}>
                      <MemberNameLink member={m} openDetail={openDetail} stopPropagation />
                    </span>
                    <TierChip tier={m.tier} />
                  </div>
                  <div style={{ display: 'flex', gap: 8, fontSize: 11, color: 'var(--ink-500)', fontFamily: 'var(--f-mono)' }}>
                    <span>시도 <b style={{ color: 'var(--ink-900)' }}>{m.matchTry || 0}</b></span>
                    <span style={{ color: '#14855a' }}>성공 <b>{m.matchSuccess || 0}</b></span>
                    <span style={{ color: '#a8281d' }}>실패 <b>{m.matchFail || 0}</b></span>
                    <span>{rate}%</span>
                  </div>
                  {m.totalPaid > 0 && (
                    <div style={{ marginTop: 4, fontSize: 11, fontFamily: 'var(--f-mono)', color: 'var(--brand-ink)' }}>
                      ₩{m.totalPaid.toLocaleString()}
                    </div>
                  )}
                </div>
                <div style={{ color: 'var(--ink-300)' }}>›</div>
              </button>
            );
          })}
        </div>

        {filtered.length === 0 && (
          <div style={{ padding: 60, textAlign: 'center', color: 'var(--ink-400)', fontSize: 14 }}>
            조건에 맞는 회원이 없어요.
          </div>
        )}
      </div>
    </AdminShell>
  );
}

// ─────────────────────────────────────────────────────────────
// 활동 로그 상세 — 회원별 매칭 기록 + 결제 내역
// ─────────────────────────────────────────────────────────────
function AdminActivityDetail({ go, member, members, seenNewMemberIds = [], openDetail }) {
  if (!member) return null;

  const rate = member.matchTry > 0 ? Math.round((member.matchSuccess / member.matchTry) * 100) : 0;

  // Generate a fake match timeline based on stats
  const fakeMatches = React.useMemo(() => {
    const out = [];
    const opp = members.filter(m => m.gender !== member.gender && m.id !== member.id);
    const total = (member.matchSuccess || 0) + (member.matchFail || 0) + (member.matchOngoing || 0);
    for (let i = 0; i < total; i++) {
      const p = opp[i % opp.length];
      let status;
      if (i < member.matchSuccess) status = 'success';
      else if (i < member.matchSuccess + member.matchFail) status = 'fail';
      else status = 'ongoing';
      out.push({
        id: `mt-${i}`,
        partner: p,
        status,
        date: `26.0${Math.max(1, 5 - Math.floor(i / 2))}.${10 + i * 3}`.slice(0, 8),
      });
    }
    return out;
  }, [member, members]);

  return (
    <AdminShell go={go} active="log-detail" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">
            <button onClick={() => go('admin-log')} style={{ color: 'var(--ink-500)' }}>활동 로그</button>
            <span>›</span>
            <span style={{ color: 'var(--ink-700)' }}>{member.id}</span>
          </div>
          <h1>
            <MemberNameLink member={member} openDetail={openDetail} />
            <span style={{ fontSize: 18, color: 'var(--ink-500)', fontWeight: 500, marginLeft: 8 }}>활동 기록</span>
          </h1>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn ghost sm" onClick={() => go('admin-log')}>← 목록</button>
        </div>
      </div>

      {/* Hero */}
      <div className="adm-card dark" style={{ marginBottom: 16, position: 'relative', overflow: 'hidden' }}>
        <div style={{
          position: 'absolute', top: -60, right: -60, width: 280, height: 280, borderRadius: 999,
          background: {
            diamond: 'rgba(200,184,255,0.3)', black: 'rgba(255,255,255,0.05)',
            red: 'rgba(226,58,46,0.3)', blue: 'rgba(47,108,246,0.3)',
            yellow: 'rgba(245,196,25,0.3)', white: 'rgba(245,239,226,0.4)',
          }[member.tier], filter: 'blur(60px)',
        }} />
        <div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 22, flexWrap: 'wrap' }}>
          <div className="av" style={{
            width: 76, height: 76, borderRadius: 18, background: tierAvatarBg(member.tier),
            color: tierAvatarColor(member.tier),
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 30, fontWeight: 700,
            border: member.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
          }}>{member.name.charAt(0)}</div>
          <div style={{ flex: 1, minWidth: 200 }}>
            <TierChip tier={member.tier} />
            <div style={{ marginTop: 8, fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em' }}>
              <MemberNameLink member={member} openDetail={openDetail} />
              <span style={{ color: 'rgba(255,255,255,0.6)', fontWeight: 500, fontSize: 18, marginLeft: 8 }}>{member.age}, {member.gender === 'M' ? '남' : '여'}</span>
            </div>
            <div style={{ marginTop: 2, color: 'rgba(255,255,255,0.6)', fontSize: 13 }}>{member.job} · {member.id}</div>
          </div>
          <button
            type="button"
            onClick={() => openDetail?.(member.id)}
            style={{
              background: 'rgba(255,255,255,0.08)', color: '#fff',
              padding: '10px 16px', borderRadius: 999,
              fontSize: 13, fontWeight: 600,
              border: '1px solid rgba(255,255,255,0.12)',
              cursor: 'pointer',
            }}
          >회원 프로필 보기 →</button>
        </div>
      </div>

      {/* Stats KPIs */}
      <div className="adm-grid-4" style={{ marginBottom: 16 }}>
        <KPI label="매칭 시도" value={member.matchTry || 0} unit="회" />
        <KPI label="성공 매칭" value={member.matchSuccess || 0} unit="건" accent="#14855a" />
        <KPI label="실패 / 거절" value={member.matchFail || 0} unit="건" accent="#ec4438" />
        <KPI label="진행 중" value={member.matchOngoing || 0} unit="건" accent="#2f6cf6" />
      </div>

      {/* Success rate visualization */}
      <div className="adm-card" style={{ marginBottom: 16, padding: 22 }}>
        <div className="tiny">성공률</div>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 6, marginBottom: 14 }}>
          <div style={{ fontSize: 38, fontWeight: 700, letterSpacing: '-0.025em' }}>{rate}<span style={{ fontSize: 18, color: 'var(--ink-500)' }}>%</span></div>
          <div style={{ fontSize: 13, color: 'var(--ink-500)' }}>
            {member.matchTry || 0}회 시도 중 {member.matchSuccess || 0}회 성사
          </div>
        </div>
        <div style={{ display: 'flex', height: 36, borderRadius: 8, overflow: 'hidden', gap: 2 }}>
          {member.matchSuccess > 0 && (
            <div style={{
              flex: member.matchSuccess, background: '#14855a',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: '#fff', fontSize: 12, fontFamily: 'var(--f-mono)', fontWeight: 600,
            }}>성공 {member.matchSuccess}</div>
          )}
          {member.matchFail > 0 && (
            <div style={{
              flex: member.matchFail, background: '#ec4438',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: '#fff', fontSize: 12, fontFamily: 'var(--f-mono)', fontWeight: 600,
            }}>실패 {member.matchFail}</div>
          )}
          {member.matchOngoing > 0 && (
            <div style={{
              flex: member.matchOngoing, background: '#2f6cf6',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: '#fff', fontSize: 12, fontFamily: 'var(--f-mono)', fontWeight: 600,
            }}>진행 {member.matchOngoing}</div>
          )}
          {(member.matchTry || 0) === 0 && (
            <div style={{
              flex: 1, background: 'var(--bg-soft)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: 'var(--ink-400)', fontSize: 12, fontFamily: 'var(--f-mono)',
            }}>아직 매칭 기록이 없어요</div>
          )}
        </div>
      </div>

      {/* 2-column: match history + payments */}
      <div className="detail-grid">
        <div className="adm-card" style={{ padding: 22 }}>
          <div className="tiny">매칭 기록</div>
          <div className="chart-title" style={{ marginTop: 4, marginBottom: 14 }}>최근 {fakeMatches.length}건</div>
          {fakeMatches.length === 0 ? (
            <div style={{ padding: 30, textAlign: 'center', color: 'var(--ink-400)', fontSize: 13 }}>
              아직 매칭 진행 기록이 없습니다.
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {fakeMatches.map(mt => {
                const statusMap = {
                  success: { l: '성사', c: '#14855a', bg: '#e8f7ee' },
                  fail: { l: '실패', c: '#a8281d', bg: '#fee5e2' },
                  ongoing: { l: '진행중', c: '#1e4cb6', bg: '#e3edff' },
                };
                const st = statusMap[mt.status];
                return (
                  <div key={mt.id} style={{
                    display: 'flex', alignItems: 'center', gap: 12,
                    padding: '10px 12px', background: 'var(--bg-soft)', borderRadius: 12,
                  }}>
                    <div style={{ fontFamily: 'var(--f-mono)', fontSize: 11, color: 'var(--ink-500)', width: 56 }}>
                      {mt.date}
                    </div>
                    <div className="av" style={{
                      width: 34, height: 34, borderRadius: 10,
                      background: tierAvatarBg(mt.partner.tier), color: tierAvatarColor(mt.partner.tier),
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      fontSize: 13, fontWeight: 700, flexShrink: 0,
                      border: mt.partner.tier === 'white' ? '1px solid var(--tier-white-stroke)' : '0',
                    }}>{mt.partner.name.charAt(0)}</div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 13.5, fontWeight: 600 }}>
                        <MemberNameLink member={mt.partner} openDetail={openDetail} />
                        <span style={{ color: 'var(--ink-500)', fontWeight: 400, fontSize: 12, marginLeft: 6 }}>{mt.partner.age}</span>
                      </div>
                      <div style={{ fontSize: 11, color: 'var(--ink-500)' }}>
                        {TIER_LABEL[mt.partner.tier]} · {mt.partner.mbti}
                      </div>
                    </div>
                    <span style={{
                      background: st.bg, color: st.c, padding: '4px 10px', borderRadius: 999,
                      fontSize: 11, fontWeight: 700, fontFamily: 'var(--f-mono)', letterSpacing: '0.04em',
                    }}>{st.l}</span>
                  </div>
                );
              })}
            </div>
          )}
        </div>

        <div>
          {/* Payment summary */}
          <div className="adm-card" style={{ padding: 22, marginBottom: 16 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
              <div>
                <div className="tiny">결제 내역</div>
                <div className="chart-title" style={{ marginTop: 4 }}>총 ₩{(member.totalPaid || 0).toLocaleString()}</div>
              </div>
              <span style={{
                fontSize: 10, fontFamily: 'var(--f-mono)', color: 'var(--brand-ink)',
                background: 'var(--brand-soft)', padding: '4px 8px', borderRadius: 6,
                letterSpacing: '0.06em',
              }}>유료 베타</span>
            </div>

            {(member.payments || []).length === 0 ? (
              <div style={{
                marginTop: 16, padding: '24px 18px', textAlign: 'center',
                background: 'var(--bg-soft)', borderRadius: 12, color: 'var(--ink-500)', fontSize: 13,
              }}>
                결제 내역이 없어요.<br />
                <span style={{ fontSize: 11, color: 'var(--ink-400)', fontFamily: 'var(--f-mono)', letterSpacing: '0.06em' }}>
                  무료 회원
                </span>
              </div>
            ) : (
              <div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 6 }}>
                {member.payments.map((p, i) => {
                  const svc = PAYMENT_SERVICES[p.service];
                  const refund = p.status === 'refund';
                  return (
                    <div key={i} style={{
                      display: 'flex', alignItems: 'center', gap: 12,
                      padding: '10px 12px',
                      background: refund ? '#fef6f5' : '#fff',
                      border: '1px solid var(--line)', borderRadius: 12,
                      opacity: refund ? 0.7 : 1,
                    }}>
                      <div style={{
                        width: 32, height: 32, borderRadius: 8,
                        background: svc?.color || '#999', flexShrink: 0,
                      }} />
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 13, fontWeight: 600, textDecoration: refund ? 'line-through' : 'none' }}>
                          {svc?.name || p.service}
                        </div>
                        <div style={{ fontSize: 11, color: 'var(--ink-500)', fontFamily: 'var(--f-mono)' }}>
                          {p.date} · {refund ? '환불' : '결제완료'}
                        </div>
                      </div>
                      <div style={{
                        fontFamily: 'var(--f-mono)', fontSize: 14, fontWeight: 600,
                        color: refund ? 'var(--ink-400)' : 'var(--ink-900)',
                      }}>
                        ₩{p.amount.toLocaleString()}
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
          </div>

          {/* Service breakdown */}
          {(member.payments || []).length > 0 && (
            <div className="adm-card" style={{ padding: 22 }}>
              <div className="tiny">서비스별 결제</div>
              <div className="chart-title" style={{ marginTop: 4, marginBottom: 12 }}>이용한 유료 서비스</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                {Object.entries(
                  member.payments
                    .filter(p => p.status === 'paid')
                    .reduce((acc, p) => {
                      acc[p.service] = (acc[p.service] || 0) + p.amount;
                      return acc;
                    }, {})
                ).map(([sid, amount]) => {
                  const svc = PAYMENT_SERVICES[sid];
                  return (
                    <div key={sid} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                      <div style={{
                        width: 10, height: 10, borderRadius: 999,
                        background: svc?.color || '#999', flexShrink: 0,
                      }} />
                      <div style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>{svc?.name || sid}</div>
                      <div style={{ fontFamily: 'var(--f-mono)', fontSize: 13, fontWeight: 700 }}>
                        ₩{amount.toLocaleString()}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </div>
      </div>
    </AdminShell>
  );
}

// ═════════════════════════════════════════════════════════════
// 문구 관리 (Admin Message Center)
//   인스타 스토리에 올린 회원 정보를 보고 신청자가 카톡으로 연락 오면,
//   주인공에게 신청자의 정보를 정리해 보낼 카톡 멘트를 한 번에 생성한다.
// ═════════════════════════════════════════════════════════════
function AdminMessageCenter({ go, members, seenNewMemberIds = [], openDetail }) {
  // 'apply' (스토리 신청 안내) | 'notify' (선택 알림) | 'roomGreeting' (카톡방 매칭 안내)
  const [mode, setMode] = React.useState('apply');
  const [sourceId, setSourceId] = React.useState(null);
  const [applicantIds, setApplicantIds] = React.useState(() => []);
  const [chooserId, setChooserId] = React.useState(null); // notify 모드 — 선택한 사람 1명
  const [partnerAId, setPartnerAId] = React.useState(null); // roomGreeting — 회원 A
  const [partnerBId, setPartnerBId] = React.useState(null); // roomGreeting — 회원 B
  const [nameMode, setNameMode] = React.useState('mask'); // 'mask' | 'full'
  const [includeIntro, setIncludeIntro] = React.useState(true);
  const [copied, setCopied] = React.useState(false);
  const taRef = React.useRef(null);

  const source = React.useMemo(
    () => (members || []).find((m) => m.id === sourceId || m._id === sourceId) || null,
    [members, sourceId],
  );
  const applicants = React.useMemo(
    () =>
      applicantIds
        .map((id) => (members || []).find((m) => m.id === id || m._id === id))
        .filter(Boolean),
    [members, applicantIds],
  );
  const chooser = React.useMemo(
    () => (members || []).find((m) => m.id === chooserId || m._id === chooserId) || null,
    [members, chooserId],
  );
  const partnerA = React.useMemo(
    () => (members || []).find((m) => m.id === partnerAId || m._id === partnerAId) || null,
    [members, partnerAId],
  );
  const partnerB = React.useMemo(
    () => (members || []).find((m) => m.id === partnerBId || m._id === partnerBId) || null,
    [members, partnerBId],
  );

  /** notify — '선택한 사람' 검색: 받는 사람 본인 제외 + (성별 있으면) 반대 성별만 */
  const membersForNotifyChooser = React.useMemo(() => {
    const base = members || [];
    if (mode !== 'notify') return base;
    const sid = sourceId != null ? String(sourceId) : '';
    const opp = source?.gender === 'M' ? 'F' : source?.gender === 'F' ? 'M' : null;
    return base.filter((m) => {
      const id = String(m.id || m._id || '');
      if (sid && id === sid) return false;
      if (opp && m.gender !== opp) return false;
      return true;
    });
  }, [mode, members, sourceId, source]);

  React.useEffect(() => {
    if (mode !== 'notify' || !chooser || !source) return;
    const opp = source.gender === 'M' ? 'F' : source.gender === 'F' ? 'M' : null;
    if (opp && chooser.gender !== opp) setChooserId(null);
  }, [mode, sourceId, source, chooser]);

  const message = React.useMemo(() => {
    if (mode === 'roomGreeting') {
      if (!partnerA || !partnerB) return '';
      return buildMatchedRoomMessage({
        memberA: partnerA,
        memberB: partnerB,
        options: { includeIntro },
      });
    }
    if (!source) return '';
    if (mode === 'notify') {
      if (!chooser) return '';
      return buildSelectedNotifyMessage({
        source,
        chooser,
        options: { nameMode, includeIntro },
      });
    }
    return buildApplicantMessage({
      source,
      applicants,
      options: { nameMode, includeIntro },
    });
  }, [mode, source, applicants, chooser, partnerA, partnerB, nameMode, includeIntro]);

  const canCopy =
    mode === 'apply'
      ? !!(source && applicants.length > 0)
      : mode === 'notify'
      ? !!(source && chooser)
      : !!(partnerA && partnerB);

  const copy = async () => {
    if (!message) return;
    try {
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(message);
      else if (taRef.current) { taRef.current.select(); document.execCommand('copy'); }
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {
      if (taRef.current) taRef.current.select();
    }
  };

  const addApplicant = (m) => {
    if (!m) return;
    const id = m.id || m._id;
    if (!id) return;
    setApplicantIds((arr) => (arr.includes(id) ? arr : [...arr, id]));
  };
  const removeApplicant = (id) => setApplicantIds((arr) => arr.filter((x) => x !== id));

  return (
    <AdminShell go={go} active="messages" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">MESSAGES · 문구 관리</div>
          <h1>
            {mode === 'apply' ? '스토리 신청 안내 멘트'
              : mode === 'notify' ? '선택 알림 멘트'
              : '카톡방 매칭 안내 멘트'}
          </h1>
        </div>
        {/* 모드 토글 */}
        <div style={{
          display: 'inline-flex', background: 'var(--bg-soft)', borderRadius: 12,
          padding: 4, gap: 2, flexWrap: 'wrap',
        }}>
          {[
            { v: 'apply', l: '스토리 신청 안내' },
            { v: 'notify', l: '선택 알림' },
            { v: 'roomGreeting', l: '카톡방 매칭 안내' },
          ].map((t) => (
            <button
              key={t.v}
              onClick={() => setMode(t.v)}
              style={{
                padding: '8px 14px', borderRadius: 8,
                background: mode === t.v ? '#fff' : 'transparent',
                color: mode === t.v ? 'var(--ink-900)' : 'var(--ink-500)',
                boxShadow: mode === t.v ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
                fontSize: 13, fontWeight: 600, cursor: 'pointer', border: 0,
              }}
            >{t.l}</button>
          ))}
        </div>
      </div>

      <div style={{ marginBottom: 14, padding: '12px 14px', background: 'var(--bg-soft)', borderRadius: 12, fontSize: 13, color: 'var(--ink-700)', lineHeight: 1.55 }}>
        {mode === 'apply' ? (
          <>💡 인스타 스토리를 보고 신청한 회원의 정보를 <b>주인공</b>에게 보낼 멘트를 자동으로 만들어줍니다. 회사·연봉·연락처 같은 민감 정보는 빠지고 익명 이니셜 + 핵심 정보만 들어가요.</>
        ) : mode === 'notify' ? (
          <>💌 누군가에게 선택받은 회원에게 보낼 알림 멘트입니다. 선택받은 분(받는 사람)과 선택한 분(보낼 정보 주인)을 고르면 자동으로 만들어져요. 진행 의사 확인 → 사진 상호 전송 + 카톡방 생성 안내까지 포함됩니다.</>
        ) : (
          <>🎉 최종 매칭이 확정되어 새로 만든 <b>3자 카톡방</b>에 보낼 첫 메시지입니다. 두 회원의 풀 정보(마스킹 없음)를 서로 안내하고, 무료·기프티콘 톤과 발전 시 알려달라는 응원·축하 문구가 함께 들어가요.</>
        )}
      </div>

      <div style={{
        display: 'grid', gap: 14,
        gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr)',
      }} className="adm-messages-grid">
        {/* 1) 받는 사람 — apply: 주인공 / notify: 선택받은 사람 / roomGreeting: 회원 A */}
        {mode === 'roomGreeting' ? (
          <div className="adm-card" style={{ padding: 18 }}>
            <div className="tiny">MEMBER A · 매칭 확정 회원 ①</div>
            <div className="chart-title" style={{ marginTop: 4, marginBottom: 12 }}>풀 정보 전달</div>
            <AdminMemberPicker
              members={(members || []).filter((m) => (m.id || m._id) !== partnerBId)}
              placeholder="회원 A 검색 (이름·코드·직업·지역)"
              onPick={(m) => setPartnerAId(m?.id || m?._id || null)}
            
              openDetail={openDetail}
            />
            <div style={{ marginTop: 12 }}>
              {partnerA ? (
                <PickedMemberCard
                  member={partnerA}
                  onRemove={() => setPartnerAId(null)}
                  accent="#1e4cb6"
                  label="회원 A"
                
                  openDetail={openDetail}
                />
              ) : (
                <div style={{
                  padding: 18, textAlign: 'center',
                  background: 'var(--bg-soft)', borderRadius: 12,
                  color: 'var(--ink-400)', fontSize: 12.5, lineHeight: 1.6,
                }}>
                  매칭 확정된 회원 한 분을 선택하세요.<br />
                  이 분의 풀 정보가 카톡방에 그대로 안내됩니다.
                </div>
              )}
            </div>
          </div>
        ) : (
          <div className="adm-card" style={{ padding: 18 }}>
            <div className="tiny">{mode === 'apply' ? 'SOURCE · 스토리 주인공' : 'TO · 선택받은 사람'}</div>
            <div className="chart-title" style={{ marginTop: 4, marginBottom: 12 }}>받는 사람 1명</div>
            <AdminMemberPicker
              members={members || []}
              placeholder={mode === 'apply' ? '주인공 검색 (이름·코드·직업·지역)' : '선택받은 사람 검색'}
              onPick={(m) => setSourceId(m?.id || m?._id || null)}
              openDetail={openDetail}
            />
            <div style={{ marginTop: 12 }}>
              {source ? (
                <PickedMemberCard
                  member={source}
                  onRemove={() => setSourceId(null)}
                  accent="#1e4cb6"
                  label={mode === 'apply' ? '주인공' : '받는 사람'}
                
                  openDetail={openDetail}
                />
              ) : (
                <div style={{
                  padding: 18, textAlign: 'center',
                  background: 'var(--bg-soft)', borderRadius: 12,
                  color: 'var(--ink-400)', fontSize: 12.5, lineHeight: 1.6,
                }}>
                  {mode === 'apply' ? '주인공을' : '선택받은 회원을'} 검색해서 선택하세요.<br />
                  ↑ 검색창에서 이름·회원코드 등으로 찾을 수 있어요.
                </div>
              )}
            </div>
          </div>
        )}

        {/* 2) 보낼 정보 주인 — apply: 신청자 다중 / notify: 선택한 사람 1명 / roomGreeting: 회원 B */}
        {mode === 'roomGreeting' ? (
          <div className="adm-card" style={{ padding: 18 }}>
            <div className="tiny">MEMBER B · 매칭 확정 회원 ②</div>
            <div className="chart-title" style={{ marginTop: 4, marginBottom: 12 }}>풀 정보 전달</div>
            <AdminMemberPicker
              members={(members || []).filter((m) => (m.id || m._id) !== partnerAId)}
              placeholder="회원 B 검색 (이름·코드·직업·지역)"
              onPick={(m) => setPartnerBId(m?.id || m?._id || null)}
            
              openDetail={openDetail}
            />
            <div style={{ marginTop: 12 }}>
              {partnerB ? (
                <PickedMemberCard
                  member={partnerB}
                  onRemove={() => setPartnerBId(null)}
                  accent="#ec4438"
                  label="회원 B"
                
                  openDetail={openDetail}
                />
              ) : (
                <div style={{
                  padding: 18, textAlign: 'center',
                  background: 'var(--bg-soft)', borderRadius: 12,
                  color: 'var(--ink-400)', fontSize: 12.5, lineHeight: 1.6,
                }}>
                  다른 매칭 회원을 선택하세요.<br />
                  두 분의 정보가 마스킹 없이 카톡방에 안내됩니다.
                </div>
              )}
            </div>
          </div>
        ) : mode === 'apply' ? (
          <div className="adm-card" style={{ padding: 18 }}>
            <div className="tiny">APPLICANTS · 신청자</div>
            <div className="chart-title" style={{ marginTop: 4, marginBottom: 12 }}>
              신청자 {applicants.length}명
              {applicantIds.length > 0 && (
                <button
                  onClick={() => setApplicantIds([])}
                  style={{
                    marginLeft: 10, fontSize: 11, fontWeight: 600,
                    color: 'var(--ink-500)', background: 'transparent', border: 0,
                    cursor: 'pointer', verticalAlign: 'middle',
                  }}
                >초기화</button>
              )}
            </div>
            <AdminMemberPicker
              members={(members || []).filter((m) => !applicantIds.includes(m.id) && (m.id || m._id) !== sourceId)}
              placeholder="신청자 추가 (이름·코드·직업·지역)"
              onPick={addApplicant}
              clearOnPick
            
              openDetail={openDetail}
            />
            <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
              {applicants.length === 0 ? (
                <div style={{
                  padding: 18, textAlign: 'center',
                  background: 'var(--bg-soft)', borderRadius: 12,
                  color: 'var(--ink-400)', fontSize: 12.5, lineHeight: 1.6,
                }}>
                  신청자를 1명 이상 추가하세요.<br />
                  여러 명이 신청해 왔다면 모두 한 번에 묶어 보낼 수 있어요.
                </div>
              ) : applicants.map((m, i) => (
                <PickedMemberCard
                  key={m.id || m._id}
                  member={m}
                  onRemove={() => removeApplicant(m.id || m._id)}
                  accent="#ec4438"
                  label={`#${i + 1}`}
                
                  openDetail={openDetail}
                />
              ))}
            </div>
          </div>
        ) : (
          <div className="adm-card" style={{ padding: 18 }}>
            <div className="tiny">CHOOSER · 선택한 사람</div>
            <div className="chart-title" style={{ marginTop: 4, marginBottom: 12 }}>1명만 선택</div>
            <AdminMemberPicker
              members={membersForNotifyChooser}
              placeholder="선택한 사람 검색"
              onPick={(m) => setChooserId(m?.id || m?._id || null)}
              openDetail={openDetail}
            />
            <div style={{ marginTop: 12 }}>
              {chooser ? (
                <PickedMemberCard
                  member={chooser}
                  onRemove={() => setChooserId(null)}
                  accent="#ec4438"
                  label="선택한 분"
                
                  openDetail={openDetail}
                />
              ) : (
                <div style={{
                  padding: 18, textAlign: 'center',
                  background: 'var(--bg-soft)', borderRadius: 12,
                  color: 'var(--ink-400)', fontSize: 12.5, lineHeight: 1.6,
                }}>
                  {source && (source.gender === 'M' || source.gender === 'F') ? (
                    <>받는 분이 {source.gender === 'M' ? '남성' : '여성'}이므로, 이 목록은 <b>반대 성별</b>만 보여요.<br />
                    검색해서 골라주세요. 이 분의 정보가 받는 사람에게 익명 이니셜로 전달됩니다.</>
                  ) : (
                    <>선택한 회원을 검색해서 골라주세요.<br />
                    받는 분의 성별이 등록돼 있으면 자동으로 반대 성별만 목록에 나와요.<br />
                    이 분의 정보가 받는 사람에게 익명 이니셜로 전달됩니다.</>
                  )}
                </div>
              )}
            </div>
          </div>
        )}

        {/* 3) 자동 생성 멘트 + 옵션 + 복사 */}
        <div className="adm-card" style={{ padding: 18 }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
            <div>
              <div className="tiny">PREVIEW · 카톡 멘트</div>
              <div className="chart-title" style={{ marginTop: 4 }}>
                {mode === 'roomGreeting'
                  ? (partnerA && partnerB
                      ? `${partnerA.name} × ${partnerB.name} 카톡방 메시지`
                      : '두 회원을 모두 선택하세요')
                  : source ? `${source.name}님께 보낼 메시지`
                  : mode === 'apply' ? '주인공을 먼저 선택하세요'
                  : '받는 사람을 먼저 선택하세요'}
              </div>
            </div>
            <span style={{
              fontFamily: 'var(--f-mono)', fontSize: 11,
              color: 'var(--ink-500)',
            }}>{message.length}자</span>
          </div>

          {/* 옵션 — 카톡방 모드에선 이름 마스킹 불필요 (풀네임 고정) */}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 10 }}>
            {mode !== 'roomGreeting' && (
              <RelayToggle
                label="이름 표시"
                options={[
                  { v: 'mask', l: '이니셜 (예: 한**)' },
                  { v: 'full', l: '풀네임' },
                ]}
                value={nameMode}
                onChange={setNameMode}
              />
            )}
            <RelayCheckRow
              label="한마디(자기소개) 포함"
              checked={includeIntro}
              onToggle={() => setIncludeIntro((v) => !v)}
            />
          </div>

          <textarea
            ref={taRef}
            value={
              message ||
              (mode === 'apply'
                ? '주인공·신청자를 선택하면 멘트가 자동으로 만들어집니다.'
                : mode === 'notify'
                ? '받는 사람·선택한 사람을 고르면 멘트가 자동으로 만들어집니다.'
                : '매칭 확정된 두 회원을 고르면 카톡방 안내 멘트가 자동으로 만들어집니다.')
            }
            readOnly
            style={{
              width: '100%', minHeight: 320, resize: 'vertical',
              background: 'var(--bg-soft)', border: '1px solid var(--line)',
              borderRadius: 12, padding: 14, boxSizing: 'border-box',
              fontSize: 13.5, lineHeight: 1.7, color: 'var(--ink-900)',
              fontFamily: 'var(--f-sans)', whiteSpace: 'pre-wrap',
            }}
          />

          <div style={{
            marginTop: 10, padding: '8px 12px', borderRadius: 10,
            background: '#fff7ec', border: '1px solid #f3e3b8',
            fontSize: 11.5, color: '#8c6a05', lineHeight: 1.55,
          }}>
            {mode === 'apply' ? (
              <>보내는 항목: 이름(이니셜) · 나이 · 지역 · 키 · 직업 · 흡연 · 한마디 ·{' '}
              <b>회사·연봉·연락처는 의사 확인 후 공유</b></>
            ) : mode === 'notify' ? (
              <>보내는 항목: 이름(이니셜) · 나이 · 지역 · 키 · 직업 · 흡연 · 한마디 ·{' '}
              <b>진행 의사 확인 → 양쪽 사진 상호 전송 + 카톡방 생성 안내까지 포함</b></>
            ) : (
              <>보내는 항목: <b>풀네임 + 두 분의 풀 정보(나이·지역·키·직업·흡연·한마디)</b> · 무료 운영 · 커피 기프티콘 환영 · 연인/결혼 발전 시 알려달라는 응원·축하 문구</>
            )}
          </div>

          <div style={{
            marginTop: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap',
          }}>
            <div style={{
              fontSize: 12, fontWeight: copied ? 700 : 500,
              color: copied ? '#14855a' : 'var(--ink-500)',
            }}>
              {copied
                ? '✓ 복사됨 — 카톡에 붙여넣어 주세요'
                : `복사 → ${mode === 'apply' ? '주인공' : mode === 'notify' ? '받는 사람' : '3자 카톡방'} 대화창에 그대로 붙여넣기`}
            </div>
            <button
              className="btn primary sm"
              onClick={copy}
              disabled={!canCopy}
              style={{
                opacity: canCopy ? 1 : 0.5,
                cursor: canCopy ? 'pointer' : 'not-allowed',
                padding: '0 18px',
              }}
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ marginRight: 4 }}>
                <rect x="8" y="8" width="12" height="12" rx="2" stroke="currentColor" strokeWidth="2"/>
                <path d="M4 16V6a2 2 0 012-2h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
              </svg>
              {copied ? '복사됨' : '카톡 멘트 복사'}
            </button>
          </div>
        </div>
      </div>
    </AdminShell>
  );
}

/** 주인공·신청자 검색기 — 결과 클릭 시 onPick(member) */
function AdminMemberPicker({ members, placeholder = '이름·코드·직업·지역으로 검색', onPick, clearOnPick, openDetail }) {
  const [q, setQ] = React.useState('');
  const [open, setOpen] = React.useState(false);
  const [active, setActive] = React.useState(0);
  const boxRef = React.useRef(null);

  const MAX_DROPDOWN = 50;
  const { list, totalMatched } = React.useMemo(() => {
    const arr = Array.isArray(members) ? members : [];
    const needle = String(q || '').trim().toLowerCase();
    const base = needle
      ? arr.filter((m) => {
          if (!m) return false;
          const fields = [m.name, m.memberCode, m.role, m.job, m.company, m.region, m.mbti, String(m.age || '')];
          return fields.some((f) => String(f || '').toLowerCase().includes(needle));
        })
      : arr;
    return { list: base.slice(0, MAX_DROPDOWN), totalMatched: base.length };
  }, [members, q]);

  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  const pick = (m) => {
    onPick?.(m);
    setOpen(false);
    if (clearOnPick) setQ('');
  };

  return (
    <div ref={boxRef} style={{ position: 'relative' }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8, padding: '0 10px',
        height: 38, borderRadius: 10, border: '1px solid var(--line)', background: '#fff',
        boxShadow: open ? '0 0 0 3px rgba(22,20,18,0.06)' : 'none',
        transition: 'box-shadow 120ms ease',
      }}>
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
          <circle cx="11" cy="11" r="7" stroke="#6b6359" strokeWidth="2" />
          <path d="M20 20l-3.5-3.5" stroke="#6b6359" strokeWidth="2" strokeLinecap="round" />
        </svg>
        <input
          value={q}
          onChange={(e) => { setQ(e.target.value); setOpen(true); setActive(0); }}
          onFocus={() => setOpen(true)}
          onKeyDown={(e) => {
            if (!open) return;
            if (e.key === 'ArrowDown') { e.preventDefault(); setActive((i) => Math.min(list.length - 1, i + 1)); }
            else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((i) => Math.max(0, i - 1)); }
            else if (e.key === 'Enter') {
              const m = list[active]; if (m) { e.preventDefault(); pick(m); }
            }
          }}
          placeholder={placeholder}
          style={{ flex: 1, minWidth: 0, border: 0, outline: 'none', background: 'transparent', fontSize: 13, color: 'var(--ink-900)' }}
        />
        {q && (
          <button
            type="button"
            onClick={() => { setQ(''); setActive(0); }}
            aria-label="검색어 지우기"
            style={{ border: 0, background: 'transparent', cursor: 'pointer', fontSize: 14, color: 'var(--ink-400)', padding: 4 }}
          >×</button>
        )}
      </div>
      {open && (
        <div style={{
          position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4,
          background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
          boxShadow: '0 12px 30px rgba(20,16,12,0.18)', zIndex: 30,
          maxHeight: 480, display: 'flex', flexDirection: 'column',
        }}>
          <div style={{ flex: 1, overflowY: 'auto' }}>
          {list.length === 0 ? (
            <div style={{ padding: 16, textAlign: 'center', fontSize: 12, color: 'var(--ink-400)' }}>
              일치하는 회원이 없습니다.
            </div>
          ) : list.map((m, i) => {
            const sido = (m.region || '').split(' ')[0];
            const isActive = i === active;
            return (
              <button
                type="button"
                key={m.id || m._id || i}
                onClick={() => pick(m)}
                onMouseEnter={() => setActive(i)}
                style={{
                  width: '100%', display: 'flex', alignItems: 'center', gap: 10,
                  padding: '8px 12px',
                  background: isActive ? 'var(--bg-soft)' : '#fff',
                  border: 0, borderBottom: '1px solid var(--bg-soft)',
                  cursor: 'pointer', textAlign: 'left',
                }}
              >
                <MemberPhotoAvatar member={m} size={32} radius={9} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink-900)', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                    <MemberNameLink member={m} openDetail={openDetail} stopPropagation />
                    <span style={{ color: 'var(--ink-500)', fontWeight: 500, fontSize: 11 }}>
                      {m.age ? `${m.age}` : ''}{m.gender ? ` · ${m.gender === 'M' ? '남' : '여'}` : ''}
                    </span>
                    <TierChip tier={m.tier} />
                  </div>
                  <div style={{ fontSize: 11, color: 'var(--ink-500)', marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {(m.memberCode || '').toUpperCase()}{m.memberCode ? ' · ' : ''}{sido}{m.role ? ` · ${m.role}` : ''}
                  </div>
                </div>
              </button>
            );
          })}
          </div>
          {totalMatched > list.length && (
            <div style={{
              padding: '8px 12px', borderTop: '1px solid var(--line)',
              background: 'var(--bg-soft)', fontSize: 11, color: 'var(--ink-500)',
              fontFamily: 'var(--f-mono)', textAlign: 'center',
            }}>
              상위 {list.length}명 표시 중 · 검색어를 입력해 더 좁혀보세요
              <span style={{ marginLeft: 6, color: 'var(--ink-400)' }}>(전체 {totalMatched}명 일치)</span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/** 선택된 주인공·신청자 한 명 표시 카드 */
function PickedMemberCard({ member, onRemove, accent, label, openDetail }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10,
      padding: '10px 12px', borderRadius: 12,
      background: '#fff', border: '1px solid var(--line)',
      borderLeft: `3px solid ${accent || 'var(--ink-300)'}`,
    }}>
      <MemberPhotoAvatar member={member} size={36} radius={10} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
          {label && (
            <span style={{
              fontFamily: 'var(--f-mono)', fontSize: 9, fontWeight: 800,
              background: accent || 'var(--ink-700)', color: '#fff',
              padding: '2px 6px', borderRadius: 4, letterSpacing: '0.04em',
            }}>{label}</span>
          )}
          <span style={{ fontSize: 13.5, fontWeight: 700 }}>
            <MemberNameLink member={member} openDetail={openDetail} />
          </span>
          <span style={{ color: 'var(--ink-500)', fontWeight: 500, fontSize: 11.5 }}>
            {member.age ? `${member.age}` : ''}{member.gender ? ` · ${member.gender === 'M' ? '남' : '여'}` : ''}
          </span>
          <TierChip tier={member.tier} />
        </div>
        <div style={{ fontSize: 11.5, color: 'var(--ink-500)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {member.memberCode || ''}{member.memberCode ? ' · ' : ''}
          {(member.region || '').split(' ').slice(0, 2).join(' ')}
          {member.role ? ` · ${member.role}` : ''}
        </div>
      </div>
      {onRemove && (
        <button
          onClick={onRemove}
          aria-label="제외"
          style={{
            width: 24, height: 24, borderRadius: 6, flexShrink: 0,
            background: 'var(--bg-soft)', color: 'var(--ink-500)',
            fontSize: 14, fontWeight: 700, cursor: 'pointer',
          }}
        >×</button>
      )}
    </div>
  );
}

/**
 * 주인공 → 신청자 멘트.
 *  - 사용자 요청 포맷:
 *      [1] 한**
 *          나이: 30
 *          지역: 서울 성동구
 *          키: 183cm
 *          직업: 컨설팅
 *          흡연: 비흡연자
 *          한마디: 운동, 영화, 넷플릭스 좋아합니다.
 */
function buildApplicantMessage({ source, applicants, options }) {
  const { nameMode = 'mask', includeIntro = true } = options || {};
  if (!source) return '';
  const list = Array.isArray(applicants) ? applicants : [];
  const lines = [];
  lines.push(`안녕하세요 ${source.name}님, 컬러소개팅입니다.`);
  lines.push('');
  if (list.length === 0) {
    lines.push('신청자를 추가해 주세요. (좌측에서 신청자를 선택하면 정보가 자동으로 채워집니다.)');
    return lines.join('\n');
  }
  lines.push(
    `${source.name}님 스토리를 보고 신청한 분이 ${list.length}명 있어서 정보 전달드려요. 마음에 드시는 분이 있다면 번호로 알려주세요.`,
  );
  lines.push('');

  list.forEach((c, i) => {
    const num = `[${i + 1}]`;
    const displayName = maskName(c.name, nameMode);
    lines.push(`${num} ${displayName}`);
    const ageStr = formatAgeWithBirth(c);
    if (ageStr) lines.push(`    나이: ${ageStr}`);
    if (c.region) lines.push(`    지역: ${c.region}`);
    if (typeof c.height === 'number' || c.height) lines.push(`    키: ${c.height}cm`);
    if (c.role) lines.push(`    직업: ${c.role}`);
    if (c.smoke) lines.push(`    흡연: ${c.smoke}`);
    if (includeIntro && c.intro) lines.push(`    한마디: ${fullIntro(c.intro)}`);
    lines.push('');
  });

  lines.push('자세한 정보(회사·연봉·연락처 등)는 의사 확인 후에 안내드릴게요.');
  return lines.join('\n');
}

/**
 * 선택받은 사람에게 보낼 알림 멘트.
 *  - source : 받는 사람 (선택받은 회원)
 *  - chooser: 선택한 사람 1명 — 익명 이니셜 + 핵심 정보로 전달
 *  - 진행 의사 확인 → 사진 상호 전송 + 카톡방 생성 안내 + 무료/커피 기프티콘 톤
 */
function buildSelectedNotifyMessage({ source, chooser, options }) {
  const { nameMode = 'mask', includeIntro = true } = options || {};
  if (!source || !chooser) return '';
  const lines = [];
  const displayName = maskName(chooser.name, nameMode);

  lines.push(`안녕하세요 ${source.name}님, 컬러소개팅입니다.`);
  lines.push('');
  lines.push(`좋은 소식 전해드려요. ${source.name}님 프로필 보시고 진지하게 만나보고 싶다고 연락 주신 분이 계셔서 정보 먼저 보내드립니다.`);
  lines.push('');

  lines.push(`[1] ${displayName}`);
  if (chooser.tier) lines.push(`    등급: ${TIER_LABEL[chooser.tier] || chooser.tier}`);
  const chooserAgeStr = formatAgeWithBirth(chooser);
  if (chooserAgeStr) lines.push(`    나이: ${chooserAgeStr}`);
  if (chooser.region) lines.push(`    지역: ${chooser.region}`);
  if (typeof chooser.height === 'number' || chooser.height) lines.push(`    키: ${chooser.height}cm`);
  if (chooser.role) lines.push(`    직업: ${chooser.role}`);
  if (chooser.smoke) lines.push(`    흡연: ${chooser.smoke}`);
  if (includeIntro && chooser.intro) lines.push(`    한마디: ${fullIntro(chooser.intro)}`);
  lines.push('');

  lines.push('이 분과 진행해보시겠어요? 진행하시겠다고 답주시면 양쪽에 사진이 서로 전송되고 카톡방이 만들어질 예정이에요.');
  lines.push('');
  lines.push('매칭은 소개 진행, 거절 여부와 상관없이 개인정보 삭제 또는 매칭 중단 요청을 주시지 않는, 이상 계속 매칭 제안 드립니다.');
  lines.push('');
  lines.push('참고로 컬러소개팅은 전액 무료로 운영하고 있어 어떤 비용도 받지 않습니다. 혹시 운영자(개발자) 응원하고 싶으시다면 커피 기프티콘 정도는 감사히 받겠습니다 :)');
  lines.push('');
  lines.push('부담 없이 답장 주세요. 좋은 인연 되시길 바랄게요.');

  return lines.join('\n');
}

/**
 * 최종 매칭이 확정되어 새로 만든 3자 카톡방의 첫 안내 멘트.
 *  - 최상단: [필독] 매너·신고 안내 (고정 블록)
 *  - 두 회원의 풀 정보(마스킹 없음) 양쪽 안내 — 무료 운영 + 커피 기프티콘 — 발전 시 알려달라는 응원·축하
 */
function buildMatchedRoomMessage({ memberA, memberB, options }) {
  const { includeIntro = true } = options || {};
  if (!memberA || !memberB) return '';
  const lines = [];

  const printMember = (m, idx) => {
    lines.push(`[${idx}] ${m.name}`);
    const ageStr = formatAgeWithBirth(m);
    if (ageStr) lines.push(`    나이: ${ageStr}`);
    if (m.region) lines.push(`    지역: ${m.region}`);
    if (typeof m.height === 'number' || m.height) lines.push(`    키: ${m.height}cm`);
    if (m.role) lines.push(`    직업: ${m.role}`);
    if (m.smoke) lines.push(`    흡연: ${m.smoke}`);
    if (includeIntro && m.intro) lines.push(`    한마디: ${fullIntro(m.intro)}`);
  };

  lines.push('━━━━━━━━━━━━━━━━━━━━');
  lines.push('⚠️ [필독] 매너 있는 대화 부탁드립니다');
  lines.push('━━━━━━━━━━━━━━━━━━━━');
  lines.push('대화 중 상대방에게 무례하거나 불쾌한 언행, 부적절한');
  lines.push('사진 전송 등 어떤 형태로든 괴롭힘이 발생할 경우,');
  lines.push('즉시 운영자 문의 카톡으로 알려주세요.');
  lines.push('');
  lines.push('신고 접수 즉시 사실관계를 확인한 뒤, 해당 회원에게는');
  lines.push('컬러소개팅 「영구 이용 제한」 조치가 적용됩니다.');
  lines.push('다시는 매칭 서비스를 이용하실 수 없게 됩니다.');
  lines.push('');
  lines.push('그런 일이 발생하지 않도록 두 분 모두 상대방을 존중하는');
  lines.push('매너 있는 대화 꼭 부탁드립니다 🙏');
  lines.push('━━━━━━━━━━━━━━━━━━━━');
  lines.push('');
  lines.push(`안녕하세요 ${memberA.name}님, ${memberB.name}님!`);
  lines.push('컬러소개팅입니다. 두 분이 서로 매칭이 확정되어 카톡방을 만들어드렸어요. 진심으로 축하드립니다 🎉');
  lines.push('');
  lines.push('서로의 정보 한 번 더 안내드릴게요.');
  lines.push('');
  printMember(memberA, 1);
  lines.push('');
  printMember(memberB, 2);
  lines.push('');
  lines.push('두 분 모두 진심을 담아 좋은 만남 이어가시길 응원할게요. 컬러소개팅은 전액 무료로 운영하고 있어 어떤 비용도 받지 않습니다. 혹시 운영자(개발자) 응원해주고 싶으시다면 커피 기프티콘 정도는 감사히 받겠습니다 :)');
  lines.push('');
  lines.push('그리고 이후에 좋은 인연으로 발전하셔서 연인 관계가 되시거나, 결혼 소식이 생기면 꼭 알려주세요! 진심으로 축하드리고 함께 기뻐할게요.');
  lines.push('');
  lines.push('편하게 인사 나누시고, 좋은 시작 되시길 바랍니다.');

  return lines.join('\n');
}

// ═════════════════════════════════════════════════════════════
// 매칭 기록 (Admin Match History)
//   모든 제안 그룹을 시간 역순으로 모아서 트리로 보여준다.
//   - 소스 회원 이름·코드로 검색 가능
//   - 상태별 필터(전체 / 응답 대기 / 최종 매칭 / 미선택)
//   - 각 그룹의 proposed 후보에서 직접 '최종 선택' 가능
// ═════════════════════════════════════════════════════════════
function AdminMatchHistory({ go, members, proposalGroups, onSelectFinal, onRejectCandidate, seenNewMemberIds = [], openDetail }) {
  const [q, setQ] = React.useState('');
  const [statusFilter, setStatusFilter] = React.useState('all'); // all | pending | finalized | rejected
  const [openId, setOpenId] = React.useState(null); // 펼쳐진 그룹 1개

  const groups = Array.isArray(proposalGroups) ? proposalGroups : [];

  // 소스 ObjectId → 우리 메모리의 풀 정보(사진 등)로 보강
  const memberById = React.useMemo(() => {
    const map = new Map();
    (members || []).forEach((m) => {
      if (m && m._id) map.set(String(m._id), m);
    });
    return map;
  }, [members]);

  const resolveSource = (groupSource) => {
    const sid = String((groupSource && (groupSource._id || groupSource)) || '');
    return memberById.get(sid) || groupSource || {};
  };

  const filtered = React.useMemo(() => {
    const needle = String(q || '').trim().toLowerCase();
    return groups.filter((g) => {
      const src = resolveSource(g.source);
      if (statusFilter !== 'all') {
        const cands = g.candidates || [];
        const hasFinal = cands.some((c) => c.status === 'final' || c.status === 'success' || c.status === 'ongoing');
        const hasPending = cands.some((c) => c.status === 'proposed');
        if (statusFilter === 'pending' && !hasPending) return false;
        if (statusFilter === 'finalized' && !hasFinal) return false;
        if (statusFilter === 'rejected' && (hasFinal || hasPending)) return false;
      }
      if (!needle) return true;
      const fields = [src.name, src.memberCode, src.region, src.role];
      return fields.some((f) => String(f || '').toLowerCase().includes(needle));
    });
  }, [groups, q, statusFilter, memberById]);

  // 요약 카운트
  const summary = React.useMemo(() => {
    let totalCandidates = 0;
    let totalFinal = 0;
    let pendingGroups = 0;
    groups.forEach((g) => {
      const cands = g.candidates || [];
      totalCandidates += cands.length;
      totalFinal += cands.filter((c) => c.status === 'final' || c.status === 'success' || c.status === 'ongoing').length;
      const hasFinal = cands.some((c) => c.status === 'final' || c.status === 'success' || c.status === 'ongoing');
      const hasPending = cands.some((c) => c.status === 'proposed');
      if (hasPending && !hasFinal) pendingGroups += 1;
    });
    return { total: groups.length, totalCandidates, totalFinal, pendingGroups };
  }, [groups]);

  return (
    <AdminShell go={go} active="match-history" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head">
        <div>
          <div className="crumb">MATCH HISTORY · 매칭 제안 기록</div>
          <h1>매칭 기록</h1>
        </div>
      </div>

      {/* 요약 KPI */}
      <div className="adm-grid-4" style={{ marginBottom: 14 }}>
        <KPI label="총 제안 그룹" value={summary.total.toLocaleString()} unit="건" />
        <KPI label="응답 대기" value={summary.pendingGroups.toLocaleString()} unit="건" sub="후보 중 최종 미지정" subClass="brand" accent="#ec4438" />
        <KPI label="누적 후보" value={summary.totalCandidates.toLocaleString()} unit="명" />
        <KPI label="최종 매칭" value={summary.totalFinal.toLocaleString()} unit="쌍" sub="O 표시" subClass="up" accent="#14855a" />
      </div>

      {/* 필터 바 */}
      <div className="filter-bar">
        <div className="search">
          <span style={{ color: 'var(--ink-400)' }}>⌕</span>
          <input
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="소스 회원 이름·회원코드·지역으로 검색"
          />
          {q && (
            <button
              onClick={() => setQ('')}
              style={{ color: 'var(--ink-400)', fontSize: 14, background: 'transparent', border: 0, cursor: 'pointer' }}
            >×</button>
          )}
        </div>
        <div className="seg">
          {[
            { v: 'all', l: '전체' },
            { v: 'pending', l: '응답 대기' },
            { v: 'finalized', l: '최종 매칭' },
            { v: 'rejected', l: '전원 미선택' },
          ].map((f) => (
            <button
              key={f.v}
              className={statusFilter === f.v ? 'on' : ''}
              onClick={() => setStatusFilter(f.v)}
            >{f.l}</button>
          ))}
        </div>
      </div>

      {/* 그룹 리스트 — 카드 한 줄 요약, 클릭 시 펼쳐서 트리 표시 */}
      {filtered.length === 0 ? (
        <div className="adm-card" style={{ padding: 40, textAlign: 'center', color: 'var(--ink-400)', fontSize: 14 }}>
          {groups.length === 0
            ? '아직 매칭 제안 기록이 없습니다. 매칭 진행 화면에서 후보를 골라 "이 제안 등록"으로 첫 기록을 만들어 보세요.'
            : '조건에 맞는 제안 기록이 없습니다.'}
        </div>
      ) : (
        <div className="adm-card" style={{ padding: 0, overflow: 'hidden' }}>
          {filtered.map((g, idx) => {
            const src = resolveSource(g.source);
            const isOpen = openId === g.groupId;
            const isLast = idx === filtered.length - 1;
            return (
              <MatchHistoryRow
                key={g.groupId}
                group={g}
                source={src}
                isOpen={isOpen}
                isLast={isLast}
                onToggle={() => setOpenId(isOpen ? null : g.groupId)}
                onSelectFinal={onSelectFinal}
                onReject={onRejectCandidate}
                members={members}
                openDetail={openDetail}
              />
            );
          })}
        </div>
      )}
    </AdminShell>
  );
}

/**
 * 매칭 기록 한 줄 — 요약 + 클릭 시 트리 펼침.
 *  - 요약: 소스 아바타·이름·시간 / 후보 미니 아바타 N개 (각자 O/✗/? 오버레이) / 그룹 상태 뱃지
 *  - 펼치면 동일 트리 시각화 + proposed 후보 "최종 선택" 가능
 */
function MatchHistoryRow({ group, source, isOpen, isLast, onToggle, onSelectFinal, onReject, members, openDetail }) {
  const cands = group.candidates || [];
  const finals = cands.filter((c) => c.status === 'final' || c.status === 'success' || c.status === 'ongoing');
  const rejects = cands.filter((c) => c.status === 'rejected' || c.status === 'fail');
  const pending = cands.filter((c) => c.status === 'proposed');

  const statusBadge = pending.length > 0 && finals.length === 0
    ? { label: '응답 대기', bg: '#fff7ec', color: '#8c6a05' }
    : finals.length > 0
    ? { label: `최종 ${finals.length}명`, bg: '#e3edff', color: '#1e4cb6' }
    : rejects.length === cands.length && cands.length > 0
    ? { label: '전원 미선택', bg: '#fee5e2', color: '#a8281d' }
    : { label: `${cands.length}명`, bg: 'var(--bg-soft)', color: 'var(--ink-700)' };

  const formatTimeAgo = (iso) => {
    if (!iso) return '';
    const ms = Date.now() - new Date(iso).getTime();
    if (ms < 60_000) return '방금';
    if (ms < 3600_000) return `${Math.floor(ms / 60_000)}분 전`;
    if (ms < 86_400_000) return `${Math.floor(ms / 3600_000)}시간 전`;
    return `${Math.floor(ms / 86_400_000)}일 전`;
  };

  return (
    <div style={{ borderBottom: isLast && !isOpen ? 0 : '1px solid var(--line)' }}>
      {/* 요약 행 (버튼) */}
      <button
        onClick={onToggle}
        aria-expanded={isOpen}
        style={{
          width: '100%', display: 'flex', alignItems: 'center', gap: 12,
          padding: '14px 16px', background: isOpen ? 'var(--bg-soft)' : '#fff',
          border: 0, textAlign: 'left', cursor: 'pointer',
          transition: 'background 120ms ease',
        }}
        onMouseEnter={(e) => { if (!isOpen) e.currentTarget.style.background = '#fbf8f3'; }}
        onMouseLeave={(e) => { if (!isOpen) e.currentTarget.style.background = '#fff'; }}
      >
        {/* expand 아이콘 */}
        <svg
          width="14" height="14" viewBox="0 0 24 24" fill="none"
          style={{
            flexShrink: 0,
            transform: isOpen ? 'rotate(90deg)' : 'rotate(0deg)',
            transition: 'transform 140ms ease',
          }}
        >
          <path d="M9 6l6 6-6 6" stroke="var(--ink-500)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>

        {/* 소스 회원 */}
        <MemberPhotoAvatar member={source} size={40} radius={11} />
        <div style={{ minWidth: 140, maxWidth: 200, flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <MemberNameLink member={source} openDetail={openDetail} stopPropagation style={{ fontSize: 14, fontWeight: 700 }} />
            <span style={{ color: 'var(--ink-500)', fontWeight: 500, fontSize: 12 }}>
              {source.age ? `${source.age}` : ''}{source.gender ? ` · ${source.gender === 'M' ? '남' : '여'}` : ''}
            </span>
          </div>
          <div style={{ fontSize: 11, color: 'var(--ink-500)', marginTop: 2, fontFamily: 'var(--f-mono)' }}>
            {(source.memberCode || '').toUpperCase() || '—'} · {formatTimeAgo(group.createdAt)}
          </div>
        </div>

        {/* 후보 미니 아바타 + O/X 오버레이 */}
        <div style={{
          flex: 1, minWidth: 0,
          display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap',
        }}>
          {cands.slice(0, 8).map((c) => {
            const isFinal = c.status === 'final' || c.status === 'success' || c.status === 'ongoing';
            const isReject = c.status === 'rejected' || c.status === 'fail';
            const overlayBg = isFinal ? '#1e4cb6' : isReject ? '#ec4438' : 'transparent';
            const overlayBorder = !isFinal && !isReject ? '1.5px dashed var(--ink-400)' : '0';
            return (
              <div
                key={c._id}
                title={`${c.member?.name || ''} · ${
                  isFinal ? '최종' : isReject ? '미선택' : '대기'
                }`}
                style={{ position: 'relative', width: 32, height: 32 }}
              >
                <MemberPhotoAvatar member={c.member} size={32} radius={9} />
                {/* O/X/? 오버레이 */}
                <div style={{
                  position: 'absolute', right: -3, bottom: -3,
                  width: 16, height: 16, borderRadius: 999,
                  background: overlayBg === 'transparent' ? '#fff' : overlayBg,
                  color: overlayBg === 'transparent' ? 'var(--ink-500)' : '#fff',
                  border: overlayBorder || '2px solid #fff',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 10, fontWeight: 800, lineHeight: 1,
                  boxShadow: '0 1px 3px rgba(0,0,0,0.15)',
                }}>
                  {isFinal ? '○' : isReject ? '✕' : '?'}
                </div>
              </div>
            );
          })}
          {cands.length > 8 && (
            <span style={{ fontSize: 11, color: 'var(--ink-500)', marginLeft: 4 }}>
              외 {cands.length - 8}명
            </span>
          )}
        </div>

        {/* 우측 통계/상태 */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
          <span style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            fontSize: 11, color: 'var(--ink-700)', fontFamily: 'var(--f-mono)',
          }}>
            <span style={{ color: '#1e4cb6', fontWeight: 700 }}>○ {finals.length}</span>
            <span style={{ color: '#ec4438', fontWeight: 700 }}>✕ {rejects.length}</span>
            <span style={{ color: 'var(--ink-500)', fontWeight: 700 }}>? {pending.length}</span>
          </span>
          <span style={{
            padding: '4px 10px', borderRadius: 999,
            background: statusBadge.bg, color: statusBadge.color,
            fontSize: 11, fontWeight: 700, letterSpacing: '0.02em',
          }}>{statusBadge.label}</span>
          <span style={{
            fontFamily: 'var(--f-mono)', fontSize: 10, fontWeight: 700,
            background: '#fff', color: 'var(--ink-500)',
            padding: '2px 7px', borderRadius: 5,
            border: '1px solid var(--line)', letterSpacing: '0.04em',
          }}>#{String(group.groupId || '').slice(0, 8)}</span>
        </div>
      </button>

      {/* 펼침: 트리 상세 */}
      {isOpen && (
        <div style={{ padding: '0 16px 14px', background: 'var(--bg-soft)' }}>
          <MatchProposalsHistory
            source={source}
            proposalGroups={[group]}
            onSelectFinal={onSelectFinal}
            onReject={onReject}
            members={members}
            hideHeader
            openDetail={openDetail}
          />
        </div>
      )}
    </div>
  );
}

Object.assign(window, {
  AdminLogin, AdminDashboard, AdminList, AdminDetail, AdminMatch, AdminMatchPick,
  AdminSettings, AdminActivityLog, AdminActivityDetail, AdminMessageCenter,
  AdminMatchHistory, MemberNameLink,
});