// 입점 신청 — 실서버 /admin/merchant-application (2026-09-23 신설)
//
// 파트너스(사장님 입점 공개 사이트)에서 예비 사장님이 보낸 신청을 운영자가 보고, 연락·입점 완료·반려로
// 처리합니다. 어드민이 하는 조작은 "상태 변경 + 처리 메모" 하나뿐이고, 바꿀 때마다 누가·언제·왜가
// 이력으로 남습니다(서버). 화면은 조작 뒤 늘 서버를 다시 읽어 그립니다.
//
// 계약: yumyumpickup#171. 서버 필드 이름은 아래 AF 한 곳에서만 읽습니다.
// 연락처 보관은 동의서 기준 3개월(파기 배치는 서버 몫, 아직 없음).
// 개인정보: 연락처는 목록에서 가리고(maskPhone) 상세에서만 전체(fmtPhone)를 보여 줍니다.
// 신청 단계는 최소 수집(가게명·업종·연락처·동의)입니다. 사업자번호는 받지 않고, 연락 뒤 어드민이 사장님 계정을
// 만들 때(/admin/merchant/join) 받습니다(리더 결정 2026-09-23). 그래서 이 화면에 사업자번호 칸이 없습니다.

const APPLICATION_PAGE_SIZE = 20;

// 동의 문구 판(privacyVersion) 표시 — 값이 두 형식으로 섞여 있습니다(backend #182).
//  새 형식: "SELLER_COLLECT/3 (2026-09-25)"  문서코드/판번호(시행일)
//  옛 형식: "seller-2026-09-23"              서버 상수. 이미 저장된 값은 그대로 둡니다.
// 문서 이름은 약관 종류 목록(GET /admin/terms/types)에서 찾고, 못 찾으면 코드를 그대로 씁니다.
// 옛 형식의 "seller" 는 SELLER_COLLECT 를 가리키던 상수라 여기서만 이어 줍니다(backend 확인).
//
// ⚠️ 호환층을 걷어낼 때(서버는 2026-10-31 에 옛 문자열 접수를 막습니다) **이 해석 코드는 지우지 마세요.**
// 서버는 저장된 값을 바꾸지 않습니다 — 옛 문자열로 접수된 행은 그대로 남습니다. 여기를 지우면
// 그 행들이 "seller-2026-09-23" 같은 날값으로 보입니다. 운영자가 "어느 동의서에 동의한 건지"를
// 못 읽게 되는데, 그건 되돌릴 수 없는 조작(반려·승인)의 근거라 비워 두면 안 되는 칸입니다.
// 지워도 되는 시점은 **옛 문자열로 저장된 행이 DB 에 하나도 없을 때**뿐입니다.
// dev 확인용으로 입점 신청 11 번을 남겨 두었습니다(backend 가 ops-log 에 적었습니다).
const CONSENT_LEGACY_CODE = { seller: 'SELLER_COLLECT' };
const consentVersionText = (raw, typeTitle) => {
  const v = String(raw || '').trim();
  if (!v) return null;
  const now = /^([A-Z][A-Z0-9_]*)\/(\d+)\s*\((\d{4}-\d{2}-\d{2})\)$/.exec(v);
  if (now) return `${typeTitle(now[1])} 제${now[2]}판 (${now[3]} 시행)`;
  const old = /^([a-z][a-z0-9-]*)-(\d{4}-\d{2}-\d{2})$/.exec(v);
  if (old) {
    const code = CONSENT_LEGACY_CODE[old[1]];
    return code ? `${typeTitle(code)} (${old[2]} 판)` : `${v} (옛 형식)`;
  }
  return v;   // 모르는 형식은 서버 값을 그대로 보여 줍니다.
};
// 약관 종류 이름을 한 번만 읽어 코드 → 이름으로 바꿉니다. 실패하면 코드를 그대로 씁니다.
const useTermsTitle = () => {
  const [map, setMap] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    api.live.terms.types()
      .then((list) => { if (alive) setMap(Object.fromEntries((list || []).map((t) => [t.code, t.title]))); })
      .catch(() => { if (alive) setMap({}); });
    return () => { alive = false; };
  }, []);
  return React.useCallback((code) => (map && map[code]) || code, [map]);
};

// 서버 필드 읽기 — 한 곳에 모았습니다.
const AF = {
  id:             (r) => r.applicationId,
  storeName:      (r) => r.storeName,
  phone:          (r) => r.phone,
  category:       (r) => r.category,
  status:         (r) => r.status,
  source:         (r) => r.source,
  regDate:        (r) => r.regDate,
  handledAt:      (r) => r.handledAt,
  handledBy:      (r) => r.handledBy,
  privacyAgreed:  (r) => r.privacyAgreed,
  privacyVersion: (r) => r.privacyVersion,
  agreedAt:       (r) => r.agreedAt,
  histories:      (r) => r.histories || [],
};

const APPLICATION_TABS = [
  { value: '',          label: '전체' },
  ...Object.keys(APPLICATION_STATUS).map((k) => ({ value: k, label: APPLICATION_STATUS[k].label })),
];

const Applications = () => {
  const [tab, setTab] = React.useState('');
  const [keyword, setKeyword] = React.useState('');
  const [applied, setApplied] = React.useState('');
  const [page, setPage] = React.useState(1);
  const [selectedId, setSelectedId] = React.useState(null);

  const query = React.useMemo(() => {
    const q = { page, size: APPLICATION_PAGE_SIZE };
    if (tab) q.statuses = tab;
    if (applied.trim()) q.keyword = applied.trim();
    return q;
  }, [tab, applied, page]);
  const list = useLive(() => api.live.applications.search(query), [JSON.stringify(query)]);
  useApplicationChanged(list.reload);
  const rows = (list.data && list.data.list) || [];
  const total = Number((list.data && list.data.totalCount) || 0);
  const lastPage = Math.max(1, Math.ceil(total / APPLICATION_PAGE_SIZE));
  const hasNext = page < lastPage;
  React.useEffect(() => { setPage(1); }, [tab, applied]);

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>입점 신청</h1>
          <div className="sub">파트너스에서 들어온 입점 신청을 확인하고 연락·입점 완료·반려로 처리합니다</div>
        </div>
        <div className="actions">
          <button className="btn btn-ghost btn-sm" onClick={list.reload} disabled={list.loading}><Icon name="refresh" size={14}/>새로고침</button>
        </div>
      </div>

      <div className="table-wrap" data-loading={list.loading ? 'true' : 'false'}>
        <Tabs items={APPLICATION_TABS} value={tab} onChange={setTab}/>
        <div className="table-toolbar" style={{ flexWrap:'wrap' }}>
          <input className="input" style={{ flex:1, minWidth:200, maxWidth:320 }} placeholder="가게명, 연락처"
            value={keyword} onChange={(e) => setKeyword(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') setApplied(keyword); }}/>
          <button className="btn btn-primary btn-sm" onClick={() => setApplied(keyword)}><Icon name="search" size={14}/>검색</button>
          <button className="btn btn-ghost btn-sm" onClick={() => { setKeyword(''); setApplied(''); }}>초기화</button>
        </div>
        <LiveBlock state={list} empty={rows.length === 0}
          emptyTitle={page > 1 ? '이 페이지에는 신청이 없습니다' : '조건에 맞는 입점 신청이 없습니다'}>
          <table className="table" style={{ opacity: list.loading ? 0.45 : 1 }}>
            <thead><tr>
              <th>신청번호</th><th>가게명</th><th>업종</th><th>연락처</th><th>상태</th><th>신청일시</th><th>마지막 처리</th>
            </tr></thead>
            <tbody>
              {rows.map((r) => (
                <tr key={AF.id(r)} onClick={() => setSelectedId(AF.id(r))} style={{ cursor:'pointer' }}>
                  <td className="t-mono">{AF.id(r)}</td>
                  <td style={{ fontWeight:600 }}>{AF.storeName(r) || '—'}</td>
                  <td style={{ color:'var(--text-2)' }}>{AF.category(r) || '—'}</td>
                  <td className="t-mono">{maskPhone(AF.phone(r))}</td>
                  <td><StatusBadge map={APPLICATION_STATUS} value={AF.status(r)}/></td>
                  <td style={{ fontSize:12.5, color:'var(--text-3)', whiteSpace:'nowrap' }}>{fmtDT(AF.regDate(r))}</td>
                  <td style={{ fontSize:12.5, color:'var(--text-3)', whiteSpace:'nowrap' }}>{AF.handledAt(r) ? fmtDT(AF.handledAt(r)) : '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </LiveBlock>
        <div className="table-foot">
          <span style={{ fontSize:13, color:'var(--text-3)' }}>
            총 {total}건 · {page}/{lastPage}페이지 (신청 최신순)
          </span>
          <div style={{ display:'flex', gap:6 }}>
            <button className="btn btn-ghost btn-sm" disabled={page <= 1 || list.loading} onClick={() => setPage((p) => p - 1)}><Icon name="chevLeft" size={14}/>이전</button>
            <button className="btn btn-ghost btn-sm" disabled={!hasNext || list.loading} onClick={() => setPage((p) => p + 1)}>다음<Icon name="chevRight" size={14}/></button>
          </div>
        </div>
      </div>

      {selectedId != null && (
        <ApplicationDrawer id={selectedId} onClose={() => setSelectedId(null)} onChanged={list.reload}/>
      )}
    </div>
  );
};

const ApplicationDrawer = ({ id, onClose, onChanged }) => {
  const detail = useLive(() => api.live.applications.get(id), [id]);
  const termsTitle = useTermsTitle();
  const a = detail.data && AF.id(detail.data) != null ? detail.data : null;
  const [creating, setCreating] = React.useState(false);   // 사장님 계정 발급 폼
  const [next, setNext] = React.useState('');
  const [memo, setMemo] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const status = a ? AF.status(a) : null;
  const options = (a && APPLICATION_NEXT[status]) || [];
  // 같은 상태로 보내면 상태는 그대로 두고 메모만 이력에 남습니다(계약). 이때도 메모 필수.
  const memoOnly = !!next && next === status;
  const memoRequired = next === 'REJECTED' || memoOnly;
  const memoMax = api.live.applications.MEMO_MAX;
  const canSubmit = !!next && (!memoRequired || memo.trim()) && memo.length <= memoMax && !busy;

  const submit = async () => {
    if (!canSubmit) return;
    const label = (APPLICATION_STATUS[next] || {}).label || next;
    const msg = memoOnly
      ? '상태는 그대로 두고 처리 메모만 이력에 남길까요?'
      : next === 'REJECTED'
      ? `이 입점 신청을 반려할까요?\n반려하면 되돌릴 수 없습니다. 처리 메모가 이력에 남습니다.`
      : next === 'DONE'
        ? `이 입점 신청을 "입점 완료"로 바꿀까요?\n처리가 끝난 상태라 되돌릴 수 없습니다.\n\n※ 계정은 만들어지지 않습니다. 계정까지 만들려면 "사장님 계정 발급"을 쓰세요.`
        : `상태를 "${label}"(으)로 바꿀까요? 처리 메모가 이력에 남습니다.`;
    if (!window.confirm(msg)) return;
    setBusy(true); setError('');
    try {
      await api.live.applications.setStatus(id, next, memo.trim());
      // 성공 응답만 믿지 않고 서버 값을 다시 읽어 그립니다.
      setNext(''); setMemo('');
      detail.reload(); onChanged();
    } catch (e) {
      setError(errorText(e));
    } finally {
      setBusy(false);
    }
  };

  // 서버는 오래된 순(첫 줄이 접수)으로 줍니다. 화면은 최근 처리가 위로 오게 뒤집습니다.
  const histories = a ? [...AF.histories(a)].reverse() : [];

  return (
    <Drawer open onClose={onClose} title={a ? `입점 신청 · ${AF.storeName(a) || id}` : `입점 신청 ${id}`}
      footer={<>
        {a && !creating && status === 'CONTACTED' && (
          // 서버가 **연락 완료 상태에서만** 발급을 허용합니다(3105). 이미 발급된 신청(DONE)에는
          // 버튼을 아예 두지 않아 3104 를 만날 일이 없습니다.
          <button className="btn btn-primary" style={{ marginRight:'auto' }} onClick={() => setCreating(true)}>
            <Icon name="user" size={14}/>사장님 계정 발급
          </button>
        )}
        {options.length > 0 && !creating && (
          <button className={next === 'REJECTED' ? 'btn btn-danger-ghost' : 'btn btn-primary'} disabled={!canSubmit} onClick={submit}>
            {busy ? '처리 중…' : memoOnly ? '메모 남기기' : next ? `${(APPLICATION_STATUS[next] || {}).label}(으)로 변경` : '상태를 고르세요'}
          </button>
        )}
        <button className="btn btn-ghost" onClick={onClose}>닫기</button>
      </>}>
      {error && <div style={{ marginBottom:12 }}><Notice tone="danger">처리하지 못했습니다: {error}</Notice></div>}
      <LiveBlock state={detail} empty={!a} emptyTitle="입점 신청을 찾을 수 없습니다">
        {a && (
          <div>
            <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:14 }}>
              <StatusBadge map={APPLICATION_STATUS} value={status}/>
              <span style={{ marginLeft:'auto', fontSize:13, color:'var(--text-3)' }}>{fmtDT(AF.regDate(a))} 신청</span>
            </div>
            {creating && (
              <MerchantAccountForm application={a} onClose={() => setCreating(false)}
                onCreated={() => { detail.reload(); onChanged(); }}/>
            )}
            <dl className="spec">
              <dt>신청번호</dt><dd className="t-mono">{AF.id(a)}</dd>
              <dt>가게명</dt><dd>{AF.storeName(a) || '—'}</dd>
              <dt>업종</dt><dd>{AF.category(a) || '—'}</dd>
              <dt>연락처</dt><dd className="t-mono">{fmtPhone(AF.phone(a))}</dd>
              <dt>개인정보 동의</dt><dd>{AF.privacyAgreed(a) === false ? '동의 기록 없음'
                : (AF.agreedAt(a) ? `${fmtDT(AF.agreedAt(a))} 동의` : '—')}</dd>
              <dt>동의 문구 판</dt><dd title={AF.privacyVersion(a) || ''}>
                {consentVersionText(AF.privacyVersion(a), termsTitle) || '—'}
              </dd>
              <dt>신청 경로</dt><dd>{AF.source(a) ? (APPLICATION_SOURCE[AF.source(a)] || AF.source(a)) : '—'}</dd>
              <dt>마지막 처리</dt><dd>{AF.handledAt(a) ? `${fmtDT(AF.handledAt(a))}${AF.handledBy(a) ? ` · 어드민 #${AF.handledBy(a)}` : ''}` : '—'}</dd>
            </dl>

            <SectionTitle right={`${histories.length}건`}>처리 이력</SectionTitle>
            {histories.length === 0 ? <LiveBlock state={{}} empty compact emptyTitle="처리 이력이 없습니다"/> : (
              <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
                {histories.map((h, i) => (
                  <div key={h.id || i} style={{ fontSize:13, padding:'8px 10px', border:'1px solid var(--border)', borderRadius:8 }}>
                    <div style={{ display:'flex', alignItems:'center', gap:8 }}>
                      <StatusBadge map={APPLICATION_STATUS} value={h.status}/>
                      <span style={{ color:'var(--text-2)' }}>{h.aid ? `어드민 #${h.aid}` : '신청자'}</span>
                      <span style={{ marginLeft:'auto', color:'var(--text-3)' }}>{fmtDT(h.regDate)}</span>
                    </div>
                    {h.memo && <div style={{ marginTop:6, color:'var(--text-2)', whiteSpace:'pre-wrap' }}>{h.memo}</div>}
                  </div>
                ))}
              </div>
            )}

            {options.length > 0 ? (
              <>
                <SectionTitle>상태 변경</SectionTitle>
                <div style={{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:10 }}>
                  {options.map((o) => (
                    <button key={o} type="button" className={`btn btn-sm ${next === o ? 'btn-primary' : 'btn-ghost'}`} onClick={() => setNext(o)}>
                      {(APPLICATION_STATUS[o] || {}).label || o}
                    </button>
                  ))}
                  <button type="button" className={`btn btn-sm ${memoOnly ? 'btn-primary' : 'btn-ghost'}`} onClick={() => setNext(status)}>
                    메모만 남기기
                  </button>
                </div>
                {next === 'DONE' && (
                  // "입점 완료"로 직접 바꾸면 **계정 없이** 끝 상태가 됩니다. 계정 발급을 거치면 서버가
                  // 계정 생성과 상태 변경을 함께 처리하므로(backend #214), 그쪽을 권합니다.
                  <div style={{ marginBottom:10 }}>
                    <Notice tone="warn">
                      <strong>계정 없이 완료 처리됩니다.</strong> 이 버튼은 신청 상태만 바꿉니다.
                      신청자가 로그인하게 하려면 <strong>"사장님 계정 발급"</strong> 을 쓰세요 —
                      계정 발급이 성공하면 신청은 자동으로 입점 완료가 됩니다.
                    </Notice>
                  </div>
                )}
                <label style={{ display:'block', fontSize:13, fontWeight:600, marginBottom:6 }}>
                  처리 메모 {memoRequired ? '(필수)' : '(선택)'}
                </label>
                <textarea className="input" style={{ width:'100%', height:80, padding:10, resize:'vertical' }}
                  value={memo} maxLength={memoMax} onChange={(e) => setMemo(e.target.value)}
                  placeholder={memoRequired ? '반려 사유를 적어 주세요. 이력에 남습니다.' : '통화 내용 등. 이력에 남습니다.'}/>
                <div style={{ textAlign:'right', fontSize:12, color:'var(--text-3)' }}>{memo.length}/{memoMax}</div>
              </>
            ) : (
              <div style={{ marginTop:16 }}><Notice>처리가 끝난 신청입니다. 상태를 더 바꿀 수 없습니다.</Notice></div>
            )}
          </div>
        )}
      </LiveBlock>
    </Drawer>
  );
};

// 사장님 계정 발급 — POST /admin/merchant-application/{id}/account (JSON, backend #214)
//
// **계정 생성 + 신청 상태 DONE 을 한 트랜잭션**으로 처리합니다. 예전에는 둘이 따로라
// "입점 완료인데 계정 없음"이 생길 수 있었습니다. 문자 실패(3103)를 포함해 어디서 실패하든
// 전부 롤백되므로, 실패 뒤 같은 아이디로 다시 시도할 수 있습니다(backend 가 dev 에서 확인).
//
// 서버는 **연락 완료(CONTACTED) 상태에서만** 발급을 허용합니다(3105).
//
// 신청서에는 **가게명·업종·연락처**만 있습니다(사업자번호·이메일·대표자명은 받지 않기로 했습니다 — 리더 결정).
// 그래서 이름·이메일·아이디는 운영자가 통화로 확인해 직접 적습니다. 연락처만 신청서에서 채워 둡니다.
//
// 서버가 임시 비밀번호를 만들어 **문자로** 보냅니다. 화면에는 비밀번호가 오지 않습니다(서버가 주지 않습니다).
// 문자 접수가 확인되지 않으면 서버가 **계정 생성을 되돌립니다**(3103) — 그때 "계정이 만들어지지 않았습니다"를
// 분명히 적습니다. 서버 메시지만 보면 "문자만 실패하고 계정은 생겼나?"로 읽히기 때문입니다.
const MerchantAccountForm = ({ application, onClose, onCreated }) => {
  const [f, setF] = React.useState(() => ({
    loginId: '', name: '', email: '',
    phoneNumber: fmtPhone(AF.phone(application)) || '',
  }));
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const [done, setDone] = React.useState(null);
  // 폼을 열자마자 "…을 적어 주세요"를 띄우면 아무것도 안 했는데 잘못한 것처럼 보입니다.
  // 한 칸이라도 손댄 뒤부터 보여 줍니다(버튼은 처음부터 잠겨 있어 실수로 보내지는 일은 없습니다).
  const [touched, setTouched] = React.useState(false);
  const set = (k, v) => { setTouched(true); setF((x) => ({ ...x, [k]: v })); };

  const problems = [];
  if (!f.loginId.trim()) problems.push('로그인 아이디를 적어 주세요.');
  if (!f.name.trim()) problems.push('사장님 이름을 적어 주세요.');
  if (!f.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(f.email.trim())) problems.push('이메일 형식을 확인해 주세요.');
  if (f.phoneNumber.replace(/\D/g, '').length < 10) problems.push('연락처를 확인해 주세요.');

  const submit = async () => {
    if (problems.length) return;
    if (!window.confirm(
      `사장님 계정을 발급할까요?\n\n아이디: ${f.loginId.trim()}\n이름: ${f.name.trim()}\n이메일: ${f.email.trim()}\n연락처: ${f.phoneNumber.trim()}\n\n`
      + `임시 비밀번호는 서버가 만들어 연락처로 문자를 보냅니다. 화면에는 표시되지 않습니다.`)) return;
    setBusy(true); setError(''); setDone(null);
    try {
      await api.live.applications.createAccount(AF.id(application), {
        loginId: f.loginId.trim(), name: f.name.trim(),
        email: f.email.trim(), phoneNumber: f.phoneNumber.trim(),
      });
      setDone(f.phoneNumber.trim());
      onCreated && onCreated();   // 신청이 DONE 으로 바뀌었으니 상세·목록을 다시 읽습니다
    } catch (e) { setError(errorText(e)); }
    finally { setBusy(false); }
  };

  if (done) {
    return (
      <div style={{ marginBottom:14 }}>
        <Notice tone="success">
          <strong>계정을 만들었고 신청은 입점 완료가 됐습니다.</strong> 임시 비밀번호를 {done} 로 문자로 보냈습니다.
          사장님이 처음 로그인할 때 비밀번호를 바꿉니다. 가게는 사장님이 직접 등록하고, 등록하면 승인 관리에 검수 요청으로 올라옵니다.
        </Notice>
        <button className="btn btn-ghost btn-sm" style={{ marginTop:8 }} onClick={onClose}>닫기</button>
      </div>
    );
  }

  return (
    <div style={{ marginBottom:14, padding:12, border:'1px solid var(--border)', borderRadius:8 }}>
      <div style={{ fontSize:13, fontWeight:700, marginBottom:8 }}>사장님 계정 발급</div>
      {error && (
        <div style={{ marginBottom:10 }}>
          <Notice tone="danger">
            <strong>계정이 만들어지지 않았습니다.</strong> {error}
            <div style={{ marginTop:4, fontSize:12 }}>
              문자를 보내지 못하면 서버가 계정 생성을 되돌립니다. 다시 시도해도 같은 아이디를 쓸 수 있습니다.
            </div>
          </Notice>
        </div>
      )}
      <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
        <div>
          <label style={{ display:'block', fontSize:12.5, fontWeight:600, marginBottom:4 }}>로그인 아이디</label>
          <input className="input" style={{ width:'100%' }} value={f.loginId} maxLength={50}
            onChange={(e) => set('loginId', e.target.value)} placeholder="사장님이 로그인할 때 쓰는 아이디"/>
        </div>
        <div>
          <label style={{ display:'block', fontSize:12.5, fontWeight:600, marginBottom:4 }}>사장님 이름</label>
          <input className="input" style={{ width:'100%' }} value={f.name} maxLength={50}
            onChange={(e) => set('name', e.target.value)}/>
        </div>
        <div>
          <label style={{ display:'block', fontSize:12.5, fontWeight:600, marginBottom:4 }}>이메일</label>
          <input className="input" style={{ width:'100%' }} value={f.email} maxLength={100}
            onChange={(e) => set('email', e.target.value)} placeholder="신청서에 없는 값입니다 — 통화로 확인해 적으세요"/>
        </div>
        <div>
          <label style={{ display:'block', fontSize:12.5, fontWeight:600, marginBottom:4 }}>연락처 (문자 받을 번호)</label>
          <input className="input" style={{ width:'100%' }} value={f.phoneNumber} maxLength={20}
            onChange={(e) => set('phoneNumber', e.target.value)}/>
          <div style={{ fontSize:12, color:'var(--text-3)', marginTop:4 }}>
            임시 비밀번호가 이 번호로 갑니다. 문자를 보내지 못하면 계정도 만들어지지 않습니다.
          </div>
        </div>
        {touched && problems.length > 0 && <Notice tone="danger">{problems.map((p, i) => <div key={i}>{p}</div>)}</Notice>}
        <div style={{ display:'flex', gap:6 }}>
          <button className="btn btn-primary btn-sm" onClick={submit} disabled={busy || problems.length > 0}>
            {busy ? '발급 중…' : '계정 발급'}
          </button>
          <button className="btn btn-ghost btn-sm" onClick={onClose} disabled={busy}>취소</button>
        </div>
      </div>
    </div>
  );
};

Object.assign(window, { Applications });
