// ============================================================
// pages-settings.jsx — 학원 설정 (Org 도메인 편집기)
//   17개 섹션 / 4개 그룹. 각 섹션은 Org 또는 자식 컬렉션의 일부를 편집.
//   각 섹션 헤더에 매핑 엔티티명을 노출하고, 하단에 접을 수 있는
//   스키마 패널을 둬서 도메인 구조와 UI 의 1:1 대응을 보이게 한다.
//
//   기존 pages-2.jsx 의 Settings 를 overwrite (window.AdminPages2.Settings).
// ============================================================
(function () {
  const { useState, useMemo, useEffect, useRef } = React;
  const D = LessonHighData;
  const Icon = window.Icon;
  const UI = window.UI;
  const AP2 = window.AdminPages2 || {};

  // ============================================================
  // 섹션 레지스트리
  // ============================================================
  const GROUPS = [
  { id: 'tenant', label: '학원', hint: 'Tenant root' },
  { id: 'ops', label: '운영', hint: 'Operations' },
  { id: 'content', label: '콘텐츠 · 양식', hint: 'Content forms' },
  { id: 'cust', label: '고객 접점', hint: 'Customer' }];


  // SECTIONS 는 함수 호이스팅에 의존해 아래에서 정의된 컴포넌트들을 참조.
  const SECTIONS = [
  { id: 'basics', group: 'tenant', label: '기본 정보', entity: 'Org', C: () => <BasicsSection /> },
  { id: 'hours', group: 'tenant', label: '운영 시간', entity: 'Org.operatingHours', C: () => <HoursSection /> },
  { id: 'holidays', group: 'tenant', label: '휴무일 · 강사 휴가', entity: 'Holiday[] · TeacherLeave[]', C: () => <HolidaysSection /> },

  { id: 'label', group: 'ops', label: '공간 라벨', entity: 'Org.spaceLabel', C: () => <LabelSection /> },
  { id: 'rooms', group: 'ops', label: '공간', entity: 'Room[]', C: () => <RoomsSection /> },
  { id: 'teachers', group: 'ops', label: '강사', entity: 'Teacher[]', C: () => <TeachersSection /> },
  { id: 'passes', group: 'ops', label: '수업권 / 패스', entity: 'PassPlan[]', C: () => <PassPlansSection /> },
  { id: 'att-policy', group: 'ops', label: '출결 정책', entity: 'Org.attendancePolicy', C: () => <AttPolicySection /> },
  { id: 'space-policy', group: 'ops', label: '공간 예약 정책', entity: 'Org.spacePolicy', C: ({ spaceLabel }) => <SpacePolicySection spaceLabel={spaceLabel} /> },

  { id: 'consult-form', group: 'content', label: '상담일지 양식', entity: 'Org.consultFormFields', C: () => <FormBuilderSection kind="consult" /> },
  { id: 'lesson-form', group: 'content', label: '수업일지 양식', entity: 'Org.lessonFormFields', C: () => <FormBuilderSection kind="lesson" /> },
  { id: 'media', group: 'content', label: '미디어 정책', entity: 'Org.mediaPolicy', C: () => <MediaPolicySection /> },

  { id: 'notif', group: 'cust', label: '알림 템플릿', entity: 'NotificationTemplate[]', C: () => <NotifSection /> },
  { id: 'payment', group: 'cust', label: '결제 정책', entity: 'Org.paymentPolicy', C: () => <PaymentPolicySection /> },
  { id: 'token', group: 'cust', label: '접근 링크 정책', entity: 'Org.tokenPolicy', C: () => <TokenPolicySection /> },
  { id: 'campaign', group: 'cust', label: '캠페인 코드', entity: 'Campaign[]', C: ({ spaceLabel }) => <CampaignSection spaceLabel={spaceLabel} /> }];


  // ============================================================
  // Shell — page-level
  // ============================================================
  function Settings({ spaceLabel }) {
    const [active, setActive] = useState('basics');
    const [dirty, setDirty] = useState(false);
    const section = SECTIONS.find((s) => s.id === active) || SECTIONS[0];

    // Demo: 어떤 input 이든 바뀌면 dirty
    function onAnyChange() {setDirty(true);}

    return (
      <React.Fragment>
      <header className="page-header">
        <div>
          <h1 className="page-title">설정</h1>
          <div className="page-sub">
            <span className="t-mono" style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Org</span>
            <span style={{ margin: '0 8px', color: 'var(--text-tertiary)' }}>·</span>
            도메인 모델의 단일 진실의 원천
            <span style={{ margin: '0 8px', color: 'var(--text-tertiary)' }}>·</span>
            <span className="t-mono num" style={{ fontSize: 11 }}>{SECTIONS.length}</span> sections
          </div>
        </div>
        <div className="page-actions">
          <span className="t-caption" style={{ fontSize: 12, marginRight: 8, color: dirty ? 'var(--rental)' : 'var(--text-tertiary)' }}>
            {dirty ? '변경됨 · 저장 안 됨' : '저장됨'}
          </span>
          <UI.Button onClick={() => setDirty(false)}>변경 취소</UI.Button>
          <UI.Button variant="primary" icon={Icon.Check} onClick={() => setDirty(false)}>저장</UI.Button>
        </div>
      </header>

      <div className="settings-layout" style={{
          display: 'grid', gridTemplateColumns: '240px 1fr', gap: 24, alignItems: 'flex-start'
        }} onChangeCapture={onAnyChange}>
        <SettingsNav active={active} onChange={setActive} />
        <div className="col" style={{ gap: 0, minWidth: 0 }}>
          <section.C spaceLabel={spaceLabel} />
        </div>
      </div>
    </React.Fragment>);

  }

  function SettingsNav({ active, onChange }) {
    return (
      <nav className="col" style={{ gap: 18, position: 'sticky', top: 16 }}>
      {GROUPS.map((g) =>
        <div key={g.id} className="col" style={{ gap: 2 }}>
          <div style={{
            paddingLeft: 12, marginBottom: 4,
            display: 'flex', alignItems: 'baseline', justifyContent: 'space-between'
          }}>
            <span className="t-micro">{g.label}</span>
            <span className="t-mono" style={{ fontSize: 9.5, color: 'var(--text-tertiary)', textTransform: 'none', letterSpacing: '0.04em' }}>{g.hint}</span>
          </div>
          {SECTIONS.filter((s) => s.group === g.id).map((s) =>
          <button
            key={s.id}
            onClick={() => onChange(s.id)}
            className={`side-item ${active === s.id ? 'is-active' : ''}`}
            style={{ paddingLeft: 12, textAlign: 'left', fontSize: 13 }}>
            
              {s.label}
            </button>
          )}
        </div>
        )}
    </nav>);

  }

  // ============================================================
  // Reusable section chrome
  // ============================================================
  function SectionHeader({ title, entity, hint, actions }) {
    return (
      <header style={{
        display: 'flex', alignItems: 'flex-start', gap: 16,
        paddingBottom: 16, marginBottom: 20, borderBottom: '1px solid var(--border-default)'
      }}>
      <div className="col" style={{ gap: 6, flex: 1, minWidth: 0 }}>
        <div className="row" style={{ gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
          <h2 style={{ margin: 0, fontSize: 20, fontWeight: 600, letterSpacing: '-0.01em' }}>{title}</h2>
          <code style={{
              fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-tertiary)',
              padding: '2px 8px', background: 'var(--bg-subtle)',
              border: '1px solid var(--border-default)', borderRadius: 4
            }}>{entity}</code>
        </div>
        {hint && <p className="t-caption" style={{ margin: 0, maxWidth: '64ch', fontSize: 13 }}>{hint}</p>}
      </div>
      {actions && <div className="row" style={{ gap: 8 }}>{actions}</div>}
    </header>);

  }

  function SchemaPeek({ name, schema, notes }) {
    const [open, setOpen] = useState(false);
    return (
      <div className="schema-peek" style={{ marginTop: 24 }}>
      <button
          onClick={() => setOpen(!open)}
          className="schema-peek__toggle"
          type="button">
          
        <span style={{ width: 12, display: 'inline-block' }}>{open ? '▾' : '▸'}</span>
        <span>{open ? 'hide' : 'show'} schema</span>
        <code style={{ marginLeft: 8 }}>{name}</code>
      </button>
      {open &&
        <div className="schema-peek__body">
          <pre className="schema-peek__code">{schema}</pre>
          {notes && <div className="schema-peek__notes">{notes}</div>}
        </div>
        }
    </div>);

  }

  function Stack({ children, gap = 16 }) {
    return <div className="col" style={{ gap }}>{children}</div>;
  }
  function Row({ children, gap = 12, align = 'flex-start' }) {
    return <div className="row" style={{ gap, alignItems: align, flexWrap: 'wrap' }}>{children}</div>;
  }
  function Field({ label, hint, children, width }) {
    return (
      <div className="field" style={{ flex: width ? `0 0 ${width}` : 1, minWidth: 0 }}>
      <label className="field__label">{label}</label>
      {children}
      {hint && <span className="t-caption" style={{ fontSize: 11, marginTop: 4 }}>{hint}</span>}
    </div>);

  }

  // ============================================================
  // 1. BASICS — Org core fields
  // ============================================================
  function BasicsSection() {
    const [org, setOrg] = useState(() => ({ ...D.org }));
    const update = (k, v) => setOrg((o) => ({ ...o, [k]: v }));

    return (
      <Stack>
      <SectionHeader
          title="기본 정보"
          entity="Org"
          hint="학원(테넌트)의 식별·법적 정보. slug 는 회원/학부모 외부 링크 도메인을 결정한다." />
        

      <UI.Card>
        <UI.CardBody>
          <Stack gap={16}>
            <Row>
              <Field label="학원명" hint="회원·학부모 화면 상단, 알림톡 발신자에 표시">
                <input className="input" value={org.name} onChange={(e) => update('name', e.target.value)} />
              </Field>
              <Field label="식별자 (slug)" hint="외부 링크 도메인 — 변경 시 기존 토큰 URL 도 함께 갱신됨" width="240px">
                <div className="row" style={{ gap: 0, alignItems: 'stretch' }}>
                  <span style={{
                      padding: '0 10px', display: 'flex', alignItems: 'center',
                      background: 'var(--bg-subtle)', border: '1px solid var(--border-default)',
                      borderRight: 'none', borderRadius: '6px 0 0 6px',
                      fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-tertiary)'
                    }}>lessonhigh.com/c/</span>
                  <input className="input t-mono" value={org.slug}
                    style={{ borderRadius: '0 6px 6px 0', flex: 1 }}
                    onChange={(e) => update('slug', e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} />
                </div>
              </Field>
            </Row>

            <Row>
              <Field label="학원 카테고리" hint="공간 라벨 프리셋·일지 필드 추천에 활용">
                <select className="select" value={org.category} onChange={(e) => update('category', e.target.value)}>
                  <option value="vocal">보컬</option>
                  <option value="art">미술</option>
                  <option value="pt">PT · 트레이닝</option>
                  <option value="dance">댄스</option>
                  <option value="piano">피아노</option>
                  <option value="academic">학습 · 입시</option>
                  <option value="other">기타</option>
                </select>
              </Field>
              <Field label="브랜드 컬러" hint="회원 모바일·알림 헤더에 강조색으로 사용" width="180px">
                <div className="row" style={{ gap: 8, alignItems: 'center' }}>
                  <span style={{ width: 28, height: 28, borderRadius: 6, background: org.brandColor, border: '1px solid var(--border-default)', flexShrink: 0 }} />
                  <input className="input t-mono" value={org.brandColor}
                    onChange={(e) => update('brandColor', e.target.value)} />
                </div>
              </Field>
              <Field label="타임존" width="180px">
                <select className="select" value={org.timezone} onChange={(e) => update('timezone', e.target.value)}>
                  <option value="Asia/Seoul">Asia/Seoul (KST)</option>
                </select>
              </Field>
            </Row>

            <Row>
              <Field label="대표자명">
                <input className="input" value={org.ceoName} onChange={(e) => update('ceoName', e.target.value)} />
              </Field>
              <Field label="사업자등록번호" hint="세금계산서·현금영수증 발급 시 사용">
                <input className="input t-mono" value={org.bizRegNo} onChange={(e) => update('bizRegNo', e.target.value)} />
              </Field>
              <Field label="대표 전화">
                <input className="input t-mono" value={org.phone} onChange={(e) => update('phone', e.target.value)} />
              </Field>
            </Row>

            <Row>
              <Field label="대표 이메일">
                <input className="input" value={org.email} onChange={(e) => update('email', e.target.value)} />
              </Field>
              <Field label="주소" width="50%">
                <input className="input" value={org.address} onChange={(e) => update('address', e.target.value)} />
              </Field>
            </Row>

            <Row>
              <Field label="가입 플랜">
                <div className="row" style={{ gap: 10, alignItems: 'baseline' }}>
                  <UI.Badge tone="success">{org.plan.toUpperCase()}</UI.Badge>
                  <span className="t-caption" style={{ fontSize: 12 }}>가입일 {org.createdAt} · ID <code className="t-mono">{org.id}</code></span>
                </div>
              </Field>
            </Row>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org"
          schema={`Org {
  id, slug, name, category,
  ceoName, bizRegNo, phone, email, address,
  logoUrl, brandColor,
  timezone, locale, currency,
  plan, createdAt,
  // children (별도 섹션에서 편집)
  operatingHours, spaceLabel,
  consultFormFields, lessonFormFields,
  attendancePolicy, spacePolicy, mediaPolicy,
  paymentPolicy, tokenPolicy,
}`}
          notes="slug 는 unique 인덱스. 변경 시 기존 SecureAccessToken URL 의 도메인 부분이 같이 마이그레이션돼야 한다." />
        
    </Stack>);

  }

  // ============================================================
  // 2. HOURS — Org.operatingHours
  // ============================================================
  function HoursSection() {
    const [hours, setHours] = useState(() => ({ ...D.org.operatingHours }));
    const dows = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
    const labels = { mon: '월', tue: '화', wed: '수', thu: '목', fri: '금', sat: '토', sun: '일' };
    const updateDow = (dow, patch) => setHours((h) => ({ ...h, [dow]: { ...h[dow], ...patch } }));

    function applyWeekday() {
      setHours((h) => {
        const next = { ...h };
        ['mon', 'tue', 'wed', 'thu', 'fri'].forEach((d) => {next[d] = { ...next.mon };});
        return next;
      });
    }

    return (
      <Stack>
      <SectionHeader
          title="운영 시간"
          entity="Org.operatingHours"
          hint="요일별 학원 영업 시간. 모든 예약 가능 슬롯·자율 연습 시간대의 상한이다. 공간별 시간은 '공간 예약 정책'에서 따로 오버라이드."
          actions={<UI.Button size="sm" variant="ghost" onClick={applyWeekday}>월~금 일괄 적용</UI.Button>} />
        
      <UI.Card>
        <UI.CardBody flush>
          <div className="hours-table">
            <div className="hours-table__head">
              <span style={{ flex: '0 0 56px' }}>요일</span>
              <span style={{ flex: '0 0 80px' }}>휴무</span>
              <span style={{ flex: 1 }}>시작</span>
              <span style={{ flex: 1 }}>종료</span>
              <span style={{ flex: '0 0 120px', textAlign: 'right' }}>운영 시간</span>
            </div>
            {dows.map((d) => {
                const h = hours[d];
                const closed = h.closed;
                const dur = !closed && h.open && h.close ?
                minutesBetween(h.open, h.close) :
                0;
                return (
                  <div key={d} className="hours-table__row">
                  <span style={{ flex: '0 0 56px', fontWeight: 600 }}>{labels[d]}</span>
                  <div style={{ flex: '0 0 80px' }}>
                    <UI.Toggle checked={!closed} onChange={(v) => updateDow(d, { closed: !v })} />
                  </div>
                  <div style={{ flex: 1 }}>
                    <input className="input t-mono num" disabled={closed} value={h.open || ''}
                      style={{ width: 110, height: 30 }}
                      onChange={(e) => updateDow(d, { open: e.target.value })} />
                  </div>
                  <div style={{ flex: 1 }}>
                    <input className="input t-mono num" disabled={closed} value={h.close || ''}
                      style={{ width: 110, height: 30 }}
                      onChange={(e) => updateDow(d, { close: e.target.value })} />
                  </div>
                  <span style={{ flex: '0 0 120px', textAlign: 'right', color: 'var(--text-tertiary)', fontSize: 12 }}>
                    {closed ? '휴무' : `${(dur / 60).toFixed(1)} 시간`}
                  </span>
                </div>);

              })}
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.operatingHours"
          schema={`type WeekHours = {
  [dow in 'mon'|'tue'|'wed'|'thu'|'fri'|'sat'|'sun']: {
    open:   "HH:mm" | null
    close:  "HH:mm" | null
    closed: boolean
  }
}`}
          notes="closed=true 인 요일은 예약 폼에서 슬롯이 생성되지 않는다." />
    </Stack>);

  }
  function minutesBetween(start, end) {
    const [sh, sm] = start.split(':').map(Number);
    const [eh, em] = end.split(':').map(Number);
    return eh * 60 + em - (sh * 60 + sm);
  }

  // ============================================================
  // 3. HOLIDAYS — Holiday[] + TeacherLeave[]
  // ============================================================
  // Helper — 휴가 사용 일수 계산
  function calcLeaveDays(start, end, kind, startTime, endTime) {
    if (kind === 'hourly' && startTime && endTime) {
      const [sh, sm] = startTime.split(':').map(Number);
      const [eh, em] = endTime.split(':').map(Number);
      const hours = (eh * 60 + em - sh * 60 - sm) / 60;
      return Math.max(0, Math.round((hours / 8) * 100) / 100);
    }
    if (!start || !end) return 0;
    const s = new Date(start);
    const e = new Date(end);
    if (isNaN(s) || isNaN(e) || e < s) return 0;
    let days = 0;
    const cur = new Date(s);
    while (cur <= e) { days++; cur.setDate(cur.getDate() + 1); }
    if (kind === 'morning' || kind === 'afternoon') return days * 0.5;
    return days;
  }
  function leaveKindLabel(kind) {
    return { full: '종일', morning: '오전 반차', afternoon: '오후 반차', hourly: '시간 단위' }[kind] || kind;
  }
  function formatLeaveRange(l) {
    if (!l.startDate) return '—';
    if (l.startDate === l.endDate) return l.startDate;
    return `${l.startDate} ~ ${l.endDate}`;
  }

  function HolidaysSection() {
    const [hols, setHols] = useState(() => [...D.holidays]);
    const [leaves, setLeaves] = useState(() => [...D.teacherLeaves]);
    const [addOpen, setAddOpen] = useState(null); // 'holiday' | 'leave' | null
    const [draft, setDraft] = useState(null);

    function openAdd(kind) {
      setAddOpen(kind);
      setDraft(kind === 'holiday' ?
        { date: '', label: '', repeatYearly: false, source: 'custom' } :
        { teacherId: D.teachers[0].id,
          startDate: '', endDate: '',
          kind: 'full', startTime: '09:00', endTime: '12:00',
          reason: '', coveredBy: null, status: 'pending' });
    }
    function save() {
      if (addOpen === 'holiday') {
        setHols((hs) => [...hs, { id: 'h_' + Date.now(), ...draft }]);
      } else {
        const daysCount = calcLeaveDays(draft.startDate, draft.endDate, draft.kind, draft.startTime, draft.endTime);
        setLeaves((ls) => [...ls, { id: 'tl_' + Date.now(), daysCount,
          requestedAt: new Date().toISOString().slice(0, 10), approvedAt: null, ...draft }]);
      }
      setAddOpen(null); setDraft(null);
    }
    function removeHol(id) { setHols((hs) => hs.filter((h) => h.id !== id)); }
    function removeLeave(id) { setLeaves((ls) => ls.filter((l) => l.id !== id)); }
    function approveLeave(id) {
      setLeaves((ls) => ls.map((l) => l.id === id ? { ...l, status: 'approved', approvedAt: new Date().toISOString().slice(0, 10) } : l));
    }

    // 강사별 사용 일수 (이번 연도)
    const usagePerTeacher = D.teachers.map(t => {
      const ts = leaves.filter(l => l.teacherId === t.id);
      const used = ts.filter(l => l.status === 'approved').reduce((s, l) => s + (l.daysCount || 0), 0);
      const pending = ts.filter(l => l.status === 'pending').reduce((s, l) => s + (l.daysCount || 0), 0);
      const bal = D.teacherLeaveBalance?.[t.id] || { annualEntitlement: 15, carryOver: 0 };
      return { teacher: t, used, pending, entitlement: bal.annualEntitlement + (bal.carryOver || 0) };
    });

    const draftDays = draft && addOpen === 'leave'
      ? calcLeaveDays(draft.startDate, draft.endDate, draft.kind, draft.startTime, draft.endTime)
      : 0;

    return (
      <Stack>
      <SectionHeader
          title="휴무일 · 강사 휴가"
          entity="Holiday[] · TeacherLeave[]"
          hint="학원 단위 휴무(전체 정지) 와 강사 단위 휴가(해당 강사 슬롯만 비활성). 일정 페이지에 회색 처리로 표시되고, 예약 폼에서 제외된다." />
        

      <UI.Card>
        <UI.CardHeader title="학원 휴무일" subtitle="Holiday[] — 전체 운영 정지일"
          actions={<UI.Button size="sm" variant="ghost" icon={Icon.Plus} onClick={() => openAdd('holiday')}>휴무일 추가</UI.Button>} />
        <UI.CardBody flush>
          <div className="list-table">
            <div className="list-table__head">
              <span style={{ flex: '0 0 130px' }}>날짜</span>
              <span style={{ flex: 1 }}>이름</span>
              <span style={{ flex: '0 0 110px' }}>매년 반복</span>
              <span style={{ flex: '0 0 110px' }} title="공휴일 = 양력 / 음력 = 매년 날짜 바뀜 / 직접 추가 = 학원이 등록">구분</span>
              <span style={{ flex: '0 0 60px' }}></span>
            </div>
            {hols.map((h) =>
              <div key={h.id} className="list-table__row">
                <span className="t-mono num" style={{ flex: '0 0 130px' }}>{h.date}</span>
                <span style={{ flex: 1, fontWeight: 500 }}>{h.label}</span>
                <span style={{ flex: '0 0 110px' }}>
                  {h.repeatYearly ? <UI.Badge tone="success">매년</UI.Badge> : <span className="t-caption">일회성</span>}
                </span>
                <span style={{ flex: '0 0 110px' }}>
                  <UI.Badge tone="neutral">
                    {h.source === 'fixed' ? '공휴일' : h.source === 'lunar' ? '음력' : '직접 추가'}
                  </UI.Badge>
                </span>
                <span style={{ flex: '0 0 60px', textAlign: 'right' }}>
                  <UI.Button size="sm" variant="ghost" isIcon icon={Icon.X} onClick={() => removeHol(h.id)} />
                </span>
              </div>
              )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="강사 휴가" subtitle="TeacherLeave[] — range · 반차 · 시간 단위 지원"
          actions={<UI.Button size="sm" variant="ghost" icon={Icon.Plus} onClick={() => openAdd('leave')}>휴가 신청</UI.Button>} />
        <UI.CardBody flush>

          {/* 강사별 연차 사용 요약 */}
          <div className="leave-balance">
            {usagePerTeacher.map(({ teacher, used, pending, entitlement }) => {
              const pct = Math.min(100, (used / entitlement) * 100);
              return (
                <div key={teacher.id} className="leave-balance__row">
                  <UI.Avatar name={teacher.initials} tone={teacher.tone} size="sm"/>
                  <div className="leave-balance__info">
                    <div className="leave-balance__name">{teacher.name}</div>
                    <div className="leave-balance__bar">
                      <div className="leave-balance__bar-fill" style={{ width: `${pct}%` }}/>
                    </div>
                  </div>
                  <div className="leave-balance__nums">
                    <span className="leave-balance__used t-mono num">{used}</span>
                    <span className="leave-balance__total t-mono num">/ {entitlement}일</span>
                    {pending > 0 && (
                      <span className="leave-balance__pending t-mono num" title="승인 대기 중">대기 {pending}일</span>
                    )}
                  </div>
                </div>
              );
            })}
          </div>

          {/* 휴가 리스트 — 카드 그리드 */}
          <div className="leave-list">
            {leaves.map((l) => {
              const t = D.getTeacher(l.teacherId);
              const sub = l.coveredBy ? D.getTeacher(l.coveredBy) : null;
              return (
                <article key={l.id} className={`leave-card leave-card--${l.status}`}>
                  <header className="leave-card__head">
                    <div className="leave-card__teacher">
                      <UI.Avatar name={t.initials} tone={t.tone} size="sm"/>
                      <span className="leave-card__name">{t.name}</span>
                    </div>
                    <div className="leave-card__head-right">
                      <UI.Badge tone={l.status === 'approved' ? 'success' : l.status === 'pending' ? 'warning' : 'neutral'}>
                        {l.status === 'approved' ? '승인' : l.status === 'pending' ? '대기' : '취소'}
                      </UI.Badge>
                      <button className="leave-card__remove" onClick={() => removeLeave(l.id)} aria-label="삭제">
                        <Icon.X size={14}/>
                      </button>
                    </div>
                  </header>

                  <div className="leave-card__range">
                    <span className="leave-card__dates t-mono num">{formatLeaveRange(l)}</span>
                    <span className="leave-card__days t-mono num">
                      {l.daysCount}일
                    </span>
                  </div>

                  <div className="leave-card__meta">
                    <span className="leave-card__kind">
                      {leaveKindLabel(l.kind)}
                      {l.kind === 'hourly' && l.startTime && (
                        <span className="t-mono num" style={{ marginLeft: 6, color: 'var(--text-tertiary)' }}>
                          {l.startTime}–{l.endTime}
                        </span>
                      )}
                    </span>
                    {sub && (
                      <span className="leave-card__cover">
                        <Icon.User size={12}/>
                        대체 <strong>{sub.name}</strong>
                      </span>
                    )}
                  </div>

                  {l.reason && (
                    <p className="leave-card__reason">{l.reason}</p>
                  )}

                  {l.status === 'pending' && (
                    <footer className="leave-card__actions">
                      <UI.Button size="sm" variant="ghost" onClick={() => removeLeave(l.id)}>반려</UI.Button>
                      <UI.Button size="sm" variant="primary" icon={Icon.Check} onClick={() => approveLeave(l.id)}>승인</UI.Button>
                    </footer>
                  )}
                </article>
              );
            })}
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Modal open={!!addOpen} onClose={() => setAddOpen(null)}
        title={addOpen === 'holiday' ? '휴무일 추가' : '강사 휴가 추가'}
        footer={
        <React.Fragment>
            <UI.Button onClick={() => setAddOpen(null)}>취소</UI.Button>
            <UI.Button variant="primary" icon={Icon.Check} onClick={save}>저장</UI.Button>
          </React.Fragment>
        }>
        {draft && addOpen === 'holiday' &&
          <Stack>
            <Field label="날짜"><input className="input t-mono" value={draft.date} placeholder="2025-12-25"
              onChange={(e) => setDraft((d) => ({ ...d, date: e.target.value }))} /></Field>
            <Field label="이름"><input className="input" value={draft.label}
              onChange={(e) => setDraft((d) => ({ ...d, label: e.target.value }))} /></Field>
            <UI.Toggle label="매년 반복" checked={draft.repeatYearly}
            onChange={(v) => setDraft((d) => ({ ...d, repeatYearly: v }))} />
          </Stack>
          }
        {draft && addOpen === 'leave' &&
          <Stack>
            <Row>
              <Field label="강사">
                <select className="select" value={draft.teacherId}
                  onChange={(e) => setDraft((d) => ({ ...d, teacherId: e.target.value }))}>
                  {D.teachers.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
                </select>
              </Field>
              <Field label="유형">
                <select className="select" value={draft.kind}
                  onChange={(e) => setDraft((d) => ({ ...d, kind: e.target.value }))}>
                  <option value="full">종일</option>
                  <option value="morning">오전 반차</option>
                  <option value="afternoon">오후 반차</option>
                  <option value="hourly">시간 단위</option>
                </select>
              </Field>
            </Row>

            <Row>
              <Field label="시작일" hint="단일 일자라면 종료일과 동일하게">
                <input className="input t-mono num" value={draft.startDate} placeholder="2025-12-08"
                  onChange={(e) => setDraft((d) => {
                    const sd = e.target.value;
                    return { ...d, startDate: sd, endDate: d.endDate || sd };
                  })} />
              </Field>
              <Field label="종료일">
                <input className="input t-mono num" value={draft.endDate} placeholder="2025-12-10"
                  onChange={(e) => setDraft((d) => ({ ...d, endDate: e.target.value }))} />
              </Field>
            </Row>

            {draft.kind === 'hourly' && (
              <Row>
                <Field label="시작 시각">
                  <input className="input t-mono num" value={draft.startTime}
                    onChange={(e) => setDraft((d) => ({ ...d, startTime: e.target.value }))} />
                </Field>
                <Field label="종료 시각">
                  <input className="input t-mono num" value={draft.endTime}
                    onChange={(e) => setDraft((d) => ({ ...d, endTime: e.target.value }))} />
                </Field>
              </Row>
            )}

            <div className="leave-modal__summary">
              <span className="t-micro">사용 일수</span>
              <span className="t-mono num leave-modal__days">{draftDays}일</span>
              <span className="t-caption" style={{ fontSize: 12, marginLeft: 'auto' }}>
                {draft.kind === 'hourly' ? '8시간 = 1일 기준' :
                 draft.kind === 'morning' || draft.kind === 'afternoon' ? '반차 = 0.5일' :
                 '주말 포함 캘린더 일수'}
              </span>
            </div>

            <Row>
              <Field label="대체 강사" hint="휴가 기간 동안 수업을 받아줄 강사 (선택)">
                <select className="select" value={draft.coveredBy || ''}
                  onChange={(e) => setDraft((d) => ({ ...d, coveredBy: e.target.value || null }))}>
                  <option value="">— 없음</option>
                  {D.teachers.filter(t => t.id !== draft.teacherId).map((t) =>
                    <option key={t.id} value={t.id}>{t.name}</option>
                  )}
                </select>
              </Field>
              <Field label="상태">
                <select className="select" value={draft.status}
                  onChange={(e) => setDraft((d) => ({ ...d, status: e.target.value }))}>
                  <option value="pending">대기 — 원장 승인 필요</option>
                  <option value="approved">바로 승인</option>
                </select>
              </Field>
            </Row>

            <Field label="사유">
              <textarea className="textarea" value={draft.reason} rows={2}
                placeholder="예: 학교 일정, 병원 진료, 가족 여행…"
                onChange={(e) => setDraft((d) => ({ ...d, reason: e.target.value }))} />
            </Field>
          </Stack>
          }
      </UI.Modal>

      <SchemaPeek
          name="Holiday[] · TeacherLeave[] · TeacherLeaveBalance"
          schema={`Holiday {
  id, date: "YYYY-MM-DD", label, repeatYearly,
  source: 'fixed' | 'lunar' | 'custom'
}

TeacherLeave {
  id, teacherId,
  startDate: "YYYY-MM-DD",
  endDate:   "YYYY-MM-DD",   // 단일일자: startDate === endDate
  kind: 'full' | 'morning' | 'afternoon' | 'hourly',
  startTime?: "HH:mm",       // 'hourly' only
  endTime?:   "HH:mm",
  daysCount: number,         // 0.5 = 반차, 0.25 = 2시간 등
  reason: string,
  coveredBy?: TeacherId,     // 대체 강사
  status: 'pending' | 'approved' | 'cancelled',
  requestedAt, approvedAt?
}

TeacherLeaveBalance {
  teacherId,
  annualEntitlement: number,   // 연간 부여 일수
  used: number,                // 승인된 휴가 합
  pending: number,             // 대기 중인 휴가 합
  carryOver: number            // 전년도 이월
}`}
          notes="연차/휴가 best practice: range + 반차/시간단위 + 자동 일수 계산 + 결재 상태 + 대체자 + 연차 잔여 표시. 단일일자 휴가는 startDate === endDate 로 표현되어 데이터 모델이 통일된다." />
    </Stack>);

  }

  // ============================================================
  // 4. LABEL — Org.spaceLabel
  // ============================================================
  function LabelSection() {
    const [label, setLabel] = useState(() => ({ ...D.org.spaceLabel }));
    const presetKey = D.org.category;
    const preset = D.spaceLabelPresets[presetKey];

    return (
      <Stack>
      <SectionHeader
          title="공간 라벨"
          entity="Org.spaceLabel"
          hint="시스템 내부에서는 Room 으로 통일하되, UI 카피는 학원이 정한 라벨로 치환된다. 토큰 {room.singular} / {room.plural} 이 사용되는 곳: 사이드바·페이지 헤더·버튼·알림톡 본문." />
        
      <UI.Card>
        <UI.CardBody>
          <Stack>
            <Row>
              <Field label="단수형" hint="예: 연습실 / 작업실 / 스튜디오">
                <input className="input" value={label.singular} onChange={(e) => setLabel((l) => ({ ...l, singular: e.target.value }))} />
              </Field>
              <Field label="복수형">
                <input className="input" value={label.plural} onChange={(e) => setLabel((l) => ({ ...l, plural: e.target.value }))} />
              </Field>
              <Field label="대여 메뉴">
                <UI.Toggle label="회원 대여 메뉴 표시" checked={label.rentalEnabled}
                  onChange={(v) => setLabel((l) => ({ ...l, rentalEnabled: v }))}
                  hint="OFF 시 회원 모바일에서 대여 탭이 사라짐" />
              </Field>
            </Row>

            {preset && label.singular !== preset.singular &&
              <div className="policy-summary">
                <span className="t-micro">카테고리 추천</span>
                <p className="t-caption" style={{ margin: 0 }}>
                  학원 카테고리 <code className="t-mono">{presetKey}</code> 의 권장 라벨은
                  <strong> {preset.singular}</strong> / <strong>{preset.plural}</strong> 입니다.
                  <button className="link"
                  style={{ marginLeft: 8, background: 'transparent', border: 'none', color: 'var(--accent-default)', cursor: 'pointer', fontWeight: 600 }}
                  onClick={() => setLabel({ ...label, ...preset })}>
                    이대로 적용 →
                  </button>
                </p>
              </div>
              }
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="미리보기" subtitle="이 라벨이 실제 어디에 적용되는지" />
        <UI.CardBody>
          <div className="col" style={{ gap: 10, fontSize: 13 }}>
            {[
              ['사이드바 메뉴', `${label.plural} (공간 뷰)`],
              ['페이지 헤더', `${label.singular} 예약`],
              ['빈 상태 카피', `예약된 ${label.singular}이(가) 없습니다`],
              ['알림톡 본문', `김민지님이 연습실 A에 도착했어요 → ${label.singular} A에 도착했어요`],
              ['회원 모바일 탭', `${label.singular}`]].
              map(([k, v], i) =>
              <div key={i} className="row" style={{ gap: 12 }}>
                <span className="t-caption" style={{ flex: '0 0 140px', fontSize: 12 }}>{k}</span>
                <span style={{ flex: 1 }}>{v}</span>
              </div>
              )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.spaceLabel"
          schema={`spaceLabel: {
  singular: string,   // e.g. "연습실"
  plural:   string,   // e.g. "연습실들" (한국어는 보통 동일)
  rentalEnabled: boolean
}`} />
    </Stack>);

  }

  // ============================================================
  // 5. ROOMS — Room[]
  // ============================================================
  function RoomsSection() {
    const [rooms, setRooms] = useState(() => [...D.rooms]);
    const labelS = D.org.spaceLabel.singular;
    function update(id, patch) {setRooms((rs) => rs.map((r) => r.id === id ? { ...r, ...patch } : r));}
    function add() {
      const idx = rooms.length + 1;
      setRooms((rs) => [...rs, { id: 'r_' + Date.now(), name: `${labelS} ${String.fromCharCode(64 + idx)}`, capacity: 1, hours: null, active: true }]);
    }
    function remove(id) {setRooms((rs) => rs.filter((r) => r.id !== id));}

    return (
      <Stack>
      <SectionHeader
          title={`공간 (${labelS})`}
          entity="Room[]"
          hint="물리적 공간 목록. 수업·상담·대여·자율 연습 예약의 단위. capacity 가 1보다 크면 그룹 수업 가능."
          actions={<UI.Button size="sm" variant="primary" icon={Icon.Plus} onClick={add}>{labelS} 추가</UI.Button>} />
        
      <UI.Card>
        <UI.CardBody flush>
          <div className="list-table">
            <div className="list-table__head">
              <span style={{ flex: 1 }}>이름</span>
              <span style={{ flex: '0 0 90px' }}>정원</span>
              <span style={{ flex: '0 0 180px' }}>운영 시간 오버라이드</span>
              <span style={{ flex: '0 0 140px' }}>장비 / 메모</span>
              <span style={{ flex: '0 0 90px' }}>활성</span>
              <span style={{ flex: '0 0 60px' }}></span>
            </div>
            {rooms.map((r) =>
              <div key={r.id} className="list-table__row">
                <input className="input" value={r.name}
                style={{ flex: 1, height: 32 }}
                onChange={(e) => update(r.id, { name: e.target.value })} />
                <input type="number" className="input t-mono num" value={r.capacity}
                style={{ flex: '0 0 90px', height: 32, width: 90 }}
                min={1} max={20}
                onChange={(e) => update(r.id, { capacity: parseInt(e.target.value || '1', 10) })} />
                <span style={{ flex: '0 0 180px' }}>
                  {r.hours ?
                  <code className="t-mono num" style={{ fontSize: 12 }}>{r.hours.start} – {r.hours.end}</code> :
                  <span className="t-caption" style={{ fontSize: 12 }}>학원 운영 시간 따름</span>}
                </span>
                <input className="input" placeholder="마이크, 스피커..."
                style={{ flex: '0 0 140px', height: 32 }}
                value={r.equipment || ''}
                onChange={(e) => update(r.id, { equipment: e.target.value })} />
                <div style={{ flex: '0 0 90px' }}>
                  <UI.Toggle checked={r.active !== false} onChange={(v) => update(r.id, { active: v })} />
                </div>
                <span style={{ flex: '0 0 60px', textAlign: 'right' }}>
                  <UI.Button size="sm" variant="ghost" isIcon icon={Icon.X} onClick={() => remove(r.id)} />
                </span>
              </div>
              )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <div className="t-caption" style={{ fontSize: 12, paddingLeft: 4 }}>
        ※ 운영 시간 오버라이드는 <strong>공간 예약 정책</strong> 섹션에서 편집합니다 — 정책과 시간 룰이 한 곳에 모이도록.
      </div>

      <SchemaPeek
          name="Room[]"
          schema={`Room {
  id, name, capacity: int,
  hours: { start: "HH:mm", end: "HH:mm" } | null,  // null = Org.operatingHours 상속
  equipment?: string,
  color?: string,
  active: boolean,
  createdAt
}`}
          notes="비활성 공간은 예약 폼에 노출되지 않지만 기존 예약 이력은 보존된다." />
    </Stack>);

  }

  // ============================================================
  // 6. TEACHERS — Teacher[]
  // ============================================================
  const TEACHER_EMAIL_HANDLE = {
    t1: 'kang.minseo',
    t2: 'yoon.yejin',
    t3: 'song.taewoo',
  };
  function TeachersSection() {
    const [list, setList] = useState(() => D.teachers.map((t) => ({
      ...t, role: t.role || 'teacher', hireDate: t.hireDate || '2024-03-01',
      email: t.email || `${TEACHER_EMAIL_HANDLE[t.id] || t.id}@gmail.com`,
      phone: t.phone || '010-0000-0000',
      hourlyShare: t.hourlyShare ?? 50, active: t.active ?? true
    })));
    function update(id, patch) {setList((ts) => ts.map((t) => t.id === id ? { ...t, ...patch } : t));}

    return (
      <Stack>
      <SectionHeader
          title="강사"
          entity="Teacher[]"
          hint="강사 계정·시급 분배율·역할. Admin 은 설정 편집·결제 처리 가능. Teacher 는 본인 시간표·일지만."
          actions={<UI.Button size="sm" variant="primary" icon={Icon.Plus}>강사 추가</UI.Button>} />
        
      <UI.Card>
        <UI.CardBody flush>
          <div className="teacher-cards">
            {list.map((t) =>
              <article key={t.id} className={`teacher-card ${t.active === false ? 'is-inactive' : ''}`}>
                {/* Header: avatar · name+role · status toggle */}
                <header className="teacher-card__header">
                  <UI.Avatar name={t.initials} tone={t.tone} size="lg"/>
                  <div className="teacher-card__id">
                    <input className="teacher-card__name" value={t.name} placeholder="이름"
                      onChange={(e) => update(t.id, { name: e.target.value })} />
                    <div className="teacher-card__role">
                      <select className="select" value={t.role}
                        onChange={(e) => update(t.id, { role: e.target.value })}>
                        <option value="admin">원장 · Admin</option>
                        <option value="teacher">강사 · Teacher</option>
                      </select>
                      <span className={`teacher-card__status ${t.active !== false ? 'is-on' : ''}`}>
                        <span className="teacher-card__status-dot"/>
                        {t.active !== false ? '활성' : '비활성'}
                      </span>
                    </div>
                  </div>
                  <UI.Toggle checked={t.active !== false} onChange={(v) => update(t.id, { active: v })} />
                </header>

                {/* Contact block — uniform 2-col grid */}
                <div className="teacher-card__grid">
                  <label className="teacher-card__field">
                    <span className="teacher-card__label">이메일</span>
                    <input className="input" value={t.email}
                      onChange={(e) => update(t.id, { email: e.target.value })} />
                  </label>
                  <label className="teacher-card__field">
                    <span className="teacher-card__label">전화</span>
                    <input className="input t-mono" value={t.phone}
                      onChange={(e) => update(t.id, { phone: e.target.value })} />
                  </label>
                </div>

                {/* Meta block — payroll & dates */}
                <footer className="teacher-card__meta">
                  <div className="teacher-card__meta-item">
                    <span className="teacher-card__label">입사일</span>
                    <input className="input t-mono num teacher-card__inline-input" value={t.hireDate}
                      onChange={(e) => update(t.id, { hireDate: e.target.value })} />
                  </div>
                  <div className="teacher-card__meta-item">
                    <span className="teacher-card__label" title="수강료 중 강사에게 분배되는 비율">시급 분배율</span>
                    <div className="teacher-card__share">
                      <input type="number" className="input t-mono num teacher-card__inline-input"
                        value={t.hourlyShare} min={0} max={100} step={5}
                        onChange={(e) => update(t.id, { hourlyShare: parseInt(e.target.value || '0', 10) })} />
                      <span className="teacher-card__pct">%</span>
                    </div>
                  </div>
                </footer>
              </article>
            )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Teacher[]"
          schema={`Teacher {
  id, name, initials, tone, color,
  role: 'admin' | 'teacher',
  email, phone,
  hireDate, terminatedAt?,
  hourlyShare: 0..100,    // 수강료 중 강사 분배율 (%)
  active: boolean,
  authProvider: 'email-password' | 'oauth-google'
}`}
          notes="hourlyShare 는 결제 정산 리포트(향후) 의 기반. role=admin 인 계정은 설정 편집 권한을 가진다." />
    </Stack>);

  }

  // ============================================================
  // 7. PASS PLANS — PassPlan[]
  // ============================================================
  function PassPlansSection() {
    const [plans, setPlans] = useState(() => [...D.passPlans]);
    const [draft, setDraft] = useState(null);

    function newPlan() {setDraft({ type: 'SESSION', sessions: 4, validDays: 30, price: 0, color: '#1F4D3A', active: true, name: '' });}
    function edit(p) {setDraft({ ...p, isEdit: true });}
    function save() {
      if (!draft.name) return;
      if (draft.isEdit) setPlans((ps) => ps.map((p) => p.id === draft.id ? { ...p, ...draft } : p));else
      setPlans((ps) => [...ps, { ...draft, id: 'pp_' + Date.now(), membersUsing: 0 }]);
      setDraft(null);
    }
    function toggle(id) {setPlans((ps) => ps.map((p) => p.id === id ? { ...p, active: !p.active } : p));}

    return (
      <Stack>
      <SectionHeader
          title="수업권 / 패스"
          entity="PassPlan[]"
          hint="회원에게 판매할 수강권 카탈로그. 회원의 passType/total/amount 는 여기서 인스턴스화된다. 가격을 바꿔도 기존 회원의 결제 이력은 영향받지 않는다 — 새 결제부터 적용."
          actions={<UI.Button size="sm" variant="primary" icon={Icon.Plus} onClick={newPlan}>패스 추가</UI.Button>} />
        

      <UI.Card>
        <UI.CardBody flush>
          <div className="list-table">
            <div className="list-table__head">
              <span style={{ flex: '2 1 220px', minWidth: 180 }}>이름</span>
              <span style={{ flex: '0 0 80px' }}>타입</span>
              <span style={{ flex: '0 0 60px', textAlign: 'right' }}>횟수</span>
              <span style={{ flex: '0 0 76px', textAlign: 'right' }}>유효기간</span>
              <span style={{ flex: '0 0 100px', textAlign: 'right' }}>가격</span>
              <span style={{ flex: '0 0 90px', textAlign: 'right' }}>회당 단가</span>
              <span style={{ flex: '0 0 70px', textAlign: 'right' }}>사용 중</span>
              <span style={{ flex: '0 0 130px', textAlign: 'right' }}></span>
            </div>
            {plans.map((p) => {
                const perSession = p.type === 'SESSION' && p.sessions ? Math.round(p.price / p.sessions) : null;
                return (
                  <div key={p.id} className={`list-table__row ${!p.active ? 'is-dim' : ''}`}>
                  <div style={{ flex: '2 1 220px', minWidth: 180, display: 'flex', alignItems: 'center', gap: 10 }}>
                    <span style={{ width: 4, height: 28, borderRadius: 2, background: p.color, flexShrink: 0 }} />
                    <span style={{ fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</span>
                    {!p.active && <UI.Badge tone="neutral">비활성</UI.Badge>}
                  </div>
                  <span style={{ flex: '0 0 80px' }}>
                    <UI.Badge tone={p.type === 'SESSION' ? 'success' : 'warning'}>
                      {p.type === 'SESSION' ? '횟수권' : '기간권'}
                    </UI.Badge>
                  </span>
                  <span className="t-mono num" style={{ flex: '0 0 60px', textAlign: 'right' }}>
                    {p.sessions ?? '무제한'}
                  </span>
                  <span className="t-mono num" style={{ flex: '0 0 76px', textAlign: 'right' }}>{p.validDays}일</span>
                  <span className="t-mono num" style={{ flex: '0 0 100px', textAlign: 'right', fontWeight: 600 }}>
                    {p.price.toLocaleString()}원
                  </span>
                  <span className="t-mono num" style={{ flex: '0 0 90px', textAlign: 'right', color: 'var(--text-tertiary)' }}>
                    {perSession ? `${perSession.toLocaleString()}원` : '—'}
                  </span>
                  <span className="t-mono num" style={{ flex: '0 0 70px', textAlign: 'right' }}>
                    {p.membersUsing > 0 ? <strong>{p.membersUsing}명</strong> : <span className="t-caption">—</span>}
                  </span>
                  <div style={{ flex: '0 0 130px', display: 'flex', justifyContent: 'flex-end', gap: 4 }}>
                    <UI.Button size="sm" variant="ghost" onClick={() => toggle(p.id)}>{p.active ? '비활성' : '활성'}</UI.Button>
                    <UI.Button size="sm" variant="ghost" icon={Icon.Edit} onClick={() => edit(p)}>편집</UI.Button>
                  </div>
                </div>);

              })}
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Modal open={!!draft} onClose={() => setDraft(null)}
        title={draft?.isEdit ? '패스 편집' : '새 패스'}
        footer={<React.Fragment>
          <UI.Button onClick={() => setDraft(null)}>취소</UI.Button>
          <UI.Button variant="primary" icon={Icon.Check} onClick={save}>저장</UI.Button>
        </React.Fragment>}>
        {draft &&
          <Stack>
            <Field label="이름 *">
              <input className="input" value={draft.name} placeholder="주1회 4주반"
              onChange={(e) => setDraft((d) => ({ ...d, name: e.target.value }))} />
            </Field>
            <Row>
              <Field label="타입">
                <select className="select" value={draft.type}
                onChange={(e) => setDraft((d) => ({ ...d, type: e.target.value }))}>
                  <option value="SESSION">횟수권 — 정해진 회차 소진 시 만료</option>
                  <option value="PERIOD">기간권 — 기간 내 무제한</option>
                </select>
              </Field>
            </Row>
            <Row>
              {draft.type === 'SESSION' &&
              <Field label="횟수">
                  <input type="number" className="input t-mono num" value={draft.sessions || ''}
                onChange={(e) => setDraft((d) => ({ ...d, sessions: parseInt(e.target.value || '0', 10) }))} />
                </Field>
              }
              <Field label="유효기간 (일)">
                <input type="number" className="input t-mono num" value={draft.validDays}
                onChange={(e) => setDraft((d) => ({ ...d, validDays: parseInt(e.target.value || '0', 10) }))} />
              </Field>
              <Field label="가격 (원)">
                <input type="number" className="input t-mono num" value={draft.price} step={10000}
                onChange={(e) => setDraft((d) => ({ ...d, price: parseInt(e.target.value || '0', 10) }))} />
              </Field>
            </Row>
            <Field label="컬러 (회원 카드에 띠지로 표시)">
              <input className="input t-mono" value={draft.color}
              onChange={(e) => setDraft((d) => ({ ...d, color: e.target.value }))} />
            </Field>
          </Stack>
          }
      </UI.Modal>

      <SchemaPeek
          name="PassPlan[]"
          schema={`PassPlan {
  id, name,
  type: 'SESSION' | 'PERIOD',
  sessions: int | null,   // SESSION 일 때만 의미 있음
  validDays: int,
  price: int (KRW),
  color: hex,
  active: boolean,
  createdAt, archivedAt?
}

// 회원이 결제 시 인스턴스화됨
MemberPass {
  id, memberId, planId,
  total, remaining,
  startedAt, expiresAt,
  amount, paymentId
}`}
          notes="회원의 passType/total/remaining 은 사실 MemberPass 의 비정규화된 값. PassPlan 의 가격을 바꿔도 기존 MemberPass 는 그대로 — '새 결제부터 적용' 의 근거." />
    </Stack>);

  }

  // ============================================================
  // 8. ATTENDANCE POLICY — Org.attendancePolicy
  // ============================================================
  function AttPolicySection() {
    const [p, setP] = useState(() => ({ ...D.attendancePolicy }));
    const upd = (k, v) => setP((prev) => ({ ...prev, [k]: v }));

    return (
      <Stack>
      <SectionHeader
          title="출결 정책"
          entity="Org.attendancePolicy"
          hint="수업 출결 처리·노쇼 차감·키오스크 체크인 모드. 이 값이 바뀌면 모든 신규 출결 이벤트에 즉시 적용된다 (기존 이력 보존)." />
        

      <UI.Card>
        <UI.CardHeader title="차감 규칙" subtitle="언제 회원의 잔여 패스를 차감할지" />
        <UI.CardBody>
          <Stack gap={12}>
            <div style={{ padding: 12, background: 'var(--bg-canvas)', borderRadius: 6, border: '1px solid var(--border-default)' }}>
              <UI.Toggle checked={p.noShowCharge} onChange={(v) => upd('noShowCharge', v)}
                label="노쇼 시 패스 차감"
                hint="회원이 약속한 수업에 오지 않으면 1회 차감. OFF 시 노쇼는 기록만 남음." />
            </div>
            <Row>
              <Field label="지각 허용 (분)" hint="이 시간 내 도착 = 출석 (지각 표시는 됨)">
                <input type="number" className="input t-mono num" value={p.lateGraceMin}
                  min={0} max={60} style={{ width: 110 }}
                  onChange={(e) => upd('lateGraceMin', parseInt(e.target.value || '0', 10))} />
              </Field>
              <Field label="자동 노쇼 처리 (분)" hint="수업 시작 N분 후 미체크인 → 자동 노쇼">
                <input type="number" className="input t-mono num" value={p.autoNoShowAfterMin}
                  min={5} max={60} style={{ width: 110 }}
                  onChange={(e) => upd('autoNoShowAfterMin', parseInt(e.target.value || '0', 10))} />
              </Field>
            </Row>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="취소·보강 규칙" />
        <UI.CardBody>
          <Stack gap={12}>
            <Row>
              <Field label="무료 취소 마감" hint="수업 시작 N시간 전까지 취소 시 차감 없음">
                <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                  <input type="number" className="input t-mono num" value={p.cancelCutoffHrs}
                    min={0} max={72} style={{ width: 90 }}
                    onChange={(e) => upd('cancelCutoffHrs', parseInt(e.target.value || '0', 10))} />
                  <span className="t-caption">시간 전</span>
                </div>
              </Field>
              <Field label="마감 후 취소">
                <select className="select" value={p.cancelChargePolicy}
                  onChange={(e) => upd('cancelChargePolicy', e.target.value)}>
                  <option value="cutoff">마감 지나면 차감 + 보강 불가</option>
                  <option value="always">언제든 차감</option>
                  <option value="never">차감 없음</option>
                </select>
              </Field>
            </Row>
            <Row>
              <Field label="보강 허용" width="200px">
                <UI.Toggle checked={p.makeupAllowed} onChange={(v) => upd('makeupAllowed', v)}
                  label="보강 수업 허용" />
              </Field>
              <Field label="보강 가능 기간 (일)" hint="원수업으로부터 N일 이내에 보강 잡아야 함">
                <input type="number" className="input t-mono num" value={p.makeupWindowDays}
                  disabled={!p.makeupAllowed} min={0} max={60} style={{ width: 110 }}
                  onChange={(e) => upd('makeupWindowDays', parseInt(e.target.value || '0', 10))} />
              </Field>
            </Row>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="키오스크 모드" subtitle="태블릿에 띄워두는 회원 셀프 체크인 화면" />
        <UI.CardBody>
          <div className="row" style={{ gap: 6 }}>
            {[
              { v: 'last4', label: '전화번호 뒷 4자리', sub: '가장 직관적, 권장' },
              { v: 'pin4', label: '개별 PIN 4자리', sub: '회원마다 발급' },
              { v: 'disabled', label: '사용 안 함', sub: '강사가 수동 체크인' }].
              map((o) =>
              <button key={o.v}
              onClick={() => upd('kioskMode', o.v)}
              className={`policy-cost ${p.kioskMode === o.v ? 'is-on' : ''}`}>
                <span className="policy-cost__label">{o.label}</span>
                <span className="policy-cost__sub">{o.sub}</span>
              </button>
              )}
          </div>
          <div style={{ marginTop: 12 }}>
            <UI.Toggle checked={p.autoCheckInOnStart} onChange={(v) => upd('autoCheckInOnStart', v)}
              label="수업 시작 시각에 자동 출석"
              hint="강사가 따로 체크하지 않아도 시작 시각이 되면 출석 처리. 노쇼는 별도 룰로 판단." />
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.attendancePolicy"
          schema={`attendancePolicy: {
  noShowCharge: boolean,
  lateGraceMin: int,           // 0..60
  cancelCutoffHrs: int,        // 0..72
  cancelChargePolicy: 'always' | 'never' | 'cutoff',
  kioskMode: 'last4' | 'pin4' | 'disabled',
  autoCheckInOnStart: boolean,
  autoNoShowAfterMin: int,
  makeupAllowed: boolean,
  makeupWindowDays: int,
}

// Attendance 이벤트 (read model)
Attendance {
  reservationId, status, checkedInAt,
  method: 'teacher'|'kiosk-pin'|'auto',
  passDelta: -1 | -0.5 | 0,
  policySnapshot   // 차감 당시 정책 스냅샷 (감사용)
}`}
          notes="정책이 바뀌어도 기존 Attendance 의 policySnapshot 은 그대로 — 정산·민원 대응 시 당시 룰을 그대로 보여줘야 한다." />
    </Stack>);

  }

  // ============================================================
  // 9. SPACE POLICY — Org.spacePolicy (PracticePolicyEditor 재사용)
  // ============================================================
  function SpacePolicySection({ spaceLabel }) {
    const [policy, setPolicy] = useState(() => ({ ...D.spacePolicy }));
    const setP = (k, v) => setPolicy((prev) => ({ ...prev, [k]: v }));
    const Editor = AP2.PracticePolicyEditor;

    return (
      <Stack>
      <SectionHeader
          title={`${spaceLabel} 예약 정책`}
          entity="Org.spacePolicy"
          hint={`수업·상담·대여 외에 자율 연습·강사 사용 예약을 허용할지, 누가, 얼마나, 비용은 어떻게 — 모두 정책에서 제어. ${spaceLabel} 별 운영 시간 오버라이드도 여기서.`} />
        
      <UI.Card>
        <UI.CardBody>
          {Editor ? <Editor policy={policy} setP={setP} spaceLabel={spaceLabel} /> : <div>Editor not loaded</div>}
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.spacePolicy"
          schema={`spacePolicy: {
  practiceEnabled: boolean,   // 마스터 스위치
  memberCanBook: boolean,
  teacherCanBook: boolean,
  requiresApproval: boolean,
  advanceDays: int,
  minDurationMin, maxDurationMin: int,
  perMemberWeeklyMax: int,
  cancelCutoffHrs: int,
  costModel: 'free' | 'pass' | 'paid',
  passDeductRatio: 0..1,
  paidRatePerHour: int,
  allowedRoomIds: RoomId[],   // [] = 모두
  hours: { start, end }       // 기본; Room.hours 가 우선
}`} />
    </Stack>);

  }

  // ============================================================
  // 10·11. FORM BUILDER — consultFormFields / lessonFormFields
  // ============================================================
  function FormBuilderSection({ kind }) {
    const isConsult = kind === 'consult';
    const key = isConsult ? 'consultFormFields' : 'lessonFormFields';
    const [fields, setFields] = useState(() => [...D.org[key]]);

    function update(id, patch) {setFields((fs) => fs.map((f) => f.id === id ? { ...f, ...patch } : f));}
    function remove(id) {setFields((fs) => fs.filter((f) => f.id !== id));}
    function add() {
      setFields((fs) => [...fs, { id: 'f_' + Date.now(), label: '새 필드', type: 'text', required: false }]);
    }
    function move(idx, dir) {
      setFields((fs) => {
        const next = [...fs];
        const j = idx + dir;
        if (j < 0 || j >= next.length) return fs;
        [next[idx], next[j]] = [next[j], next[idx]];
        return next;
      });
    }

    return (
      <Stack>
      <SectionHeader
          title={isConsult ? '상담일지 양식' : '수업일지 양식'}
          entity={`Org.${key}`}
          hint={isConsult ?
          '상담 시 강사가 입력하는 정형 필드. 학원 카테고리에 맞춰 자유롭게 정의 — 보컬이면 음역대·선호 가수, 미술이면 선호 매체 등.' :
          '매 수업마다 강사가 채워야 하는 정형 필드. 회원 모바일 일지 화면에 그대로 노출됨.'}
          actions={<UI.Button size="sm" variant="primary" icon={Icon.Plus} onClick={add}>필드 추가</UI.Button>} />
        

      <UI.Card>
        <UI.CardBody>
          <Stack gap={8}>
            {fields.map((f, i) =>
              <div key={f.id} className="form-builder__row">
                <div className="form-builder__grip">
                  <button className="form-builder__arrow" onClick={() => move(i, -1)} disabled={i === 0}>▲</button>
                  <button className="form-builder__arrow" onClick={() => move(i, +1)} disabled={i === fields.length - 1}>▼</button>
                </div>
                <input className="input" value={f.label} style={{ flex: 1, height: 32 }}
                onChange={(e) => update(f.id, { label: e.target.value })} />
                <select className="select" value={f.type} style={{ width: 140, height: 32 }}
                onChange={(e) => update(f.id, { type: e.target.value })}>
                  <option value="text">짧은 텍스트</option>
                  <option value="longtext">긴 텍스트</option>
                  <option value="select">단일 선택</option>
                  <option value="multi">다중 선택</option>
                  <option value="number">숫자</option>
                  <option value="date">날짜</option>
                </select>
                {(f.type === 'select' || f.type === 'multi') &&
                <input className="input" placeholder="옵션을 쉼표로 구분"
                value={(f.options || []).join(', ')}
                style={{ flex: 1.2, height: 32 }}
                onChange={(e) => update(f.id, { options: e.target.value.split(',').map((s) => s.trim()).filter(Boolean) })} />
                }
                <label className="row" style={{ gap: 6, fontSize: 13, color: 'var(--text-secondary)' }}>
                  <input type="checkbox" checked={f.required}
                  onChange={(e) => update(f.id, { required: e.target.checked })} />
                  필수
                </label>
                <UI.Button size="sm" variant="ghost" isIcon icon={Icon.X} onClick={() => remove(f.id)} />
              </div>
              )}
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="미리보기" subtitle={`강사가 보는 ${isConsult ? '상담' : '수업'}일지 작성 화면`} />
        <UI.CardBody>
          <div className="col" style={{ gap: 12, maxWidth: 520 }}>
            {fields.map((f) =>
              <div key={f.id} className="field">
                <label className="field__label">
                  {f.label}
                  {f.required && <span style={{ color: 'var(--rental)' }}> *</span>}
                </label>
                {f.type === 'longtext' ? <textarea className="textarea" rows={2} /> :
                f.type === 'select' ?
                <select className="select"><option>—</option>{(f.options || []).map((o) => <option key={o}>{o}</option>)}</select> :

                f.type === 'number' ? <input type="number" className="input t-mono num" /> :
                f.type === 'date' ? <input type="text" className="input t-mono num" placeholder="2025-11-21" /> :
                <input className="input" placeholder={f.label} />}
              </div>
              )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name={`Org.${key}`}
          schema={`type FormField = {
  id, label,
  type: 'text' | 'longtext' | 'select' | 'multi' | 'number' | 'date',
  required: boolean,
  options?: string[],   // select/multi 일 때만
  order: int            // 배열 순서로 갈음
}

// 작성된 일지는 formData JSON 에 저장
ConsultJournal { id, formData: Record<FieldLabel, any>, ... }
LessonNote     { id, formData: Record<FieldLabel, any>, ... }`}
          notes="필드를 삭제해도 기존 일지의 formData 는 보존된다. UI 는 현재 정의된 필드만 렌더하지만, '예전 값' 토글로 누락 필드를 보여줄 수 있다." />
    </Stack>);

  }

  // ============================================================
  // 13. MEDIA POLICY — Org.mediaPolicy
  // ============================================================
  function MediaPolicySection() {
    const [m, setM] = useState(() => ({ ...D.mediaPolicy }));
    const upd = (k, v) => setM((p) => ({ ...p, [k]: v }));

    // 추정 스토리지 사용량
    const estPerNotePerMonth = 90 * 0.05; // 90s audio ≈ 4.5MB
    const monthlyNotes = 400;
    const monthlyGB = estPerNotePerMonth * monthlyNotes * (m.retentionDays / 30) / 1024;

    return (
      <Stack>
      <SectionHeader
          title="미디어 정책"
          entity="Org.mediaPolicy"
          hint="수업일지에 첨부되는 녹음·사진의 상한과 보관 기간. 보관 기간을 줄이면 만료된 파일은 자동 삭제된다 (만료 7일 전 알림)." />
        

      <UI.Card>
        <UI.CardBody>
          <Stack gap={16}>
            <Row>
              <Field label="녹음 최대 길이" hint="강사 폰에서 녹음 시 자동 제한">
                <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                  <input type="number" className="input t-mono num" value={m.maxAudioSec}
                    min={10} max={600} step={10} style={{ width: 100 }}
                    onChange={(e) => upd('maxAudioSec', parseInt(e.target.value || '0', 10))} />
                  <span className="t-caption">초 ({Math.round(m.maxAudioSec / 60 * 10) / 10}분)</span>
                </div>
              </Field>
              <Field label="일지 1건당 사진 최대 개수">
                <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                  <input type="number" className="input t-mono num" value={m.maxPhotoCount}
                    min={0} max={20} style={{ width: 100 }}
                    onChange={(e) => upd('maxPhotoCount', parseInt(e.target.value || '0', 10))} />
                  <span className="t-caption">장</span>
                </div>
              </Field>
            </Row>

            <Row>
              <Field label="보관 기간" hint="만료 후 자동 삭제 (회원 알림 발송됨)">
                <select className="select" value={m.retentionDays}
                  onChange={(e) => upd('retentionDays', parseInt(e.target.value, 10))}>
                  <option value={90}>90일 (3개월)</option>
                  <option value={180}>180일 (6개월)</option>
                  <option value={365}>365일 (1년) · 권장</option>
                  <option value={730}>730일 (2년)</option>
                  <option value={0}>무기한</option>
                </select>
              </Field>
              <Field label="회원 셀프 업로드">
                <UI.Toggle checked={m.memberCanUpload} onChange={(v) => upd('memberCanUpload', v)}
                  label="회원이 자기 녹음 업로드 허용"
                  hint="회원 모바일 '녹음·진도' 탭에서 직접 업로드 가능" />
              </Field>
            </Row>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="예상 스토리지" subtitle="평균 400건/월 · 녹음 1건당 ~4.5MB 가정" />
        <UI.CardBody>
          <div className="row" style={{ gap: 24 }}>
            <div className="col" style={{ gap: 4 }}>
              <span className="t-micro">월 신규</span>
              <span className="t-mono num" style={{ fontSize: 22, fontWeight: 700 }}>~{(estPerNotePerMonth * monthlyNotes / 1024).toFixed(1)} GB</span>
            </div>
            <div className="col" style={{ gap: 4 }}>
              <span className="t-micro">보관 누적 (정상 상태)</span>
              <span className="t-mono num" style={{ fontSize: 22, fontWeight: 700, color: 'var(--accent-default)' }}>~{monthlyGB.toFixed(1)} GB</span>
            </div>
            <div className="col" style={{ gap: 4 }}>
              <span className="t-micro">현재 사용 중</span>
              <span className="t-mono num" style={{ fontSize: 22, fontWeight: 700, color: 'var(--text-tertiary)' }}>14.2 GB</span>
            </div>
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.mediaPolicy"
          schema={`mediaPolicy: {
  maxAudioSec: int,
  maxPhotoCount: int,
  retentionDays: int,         // 0 = 무기한
  memberCanUpload: boolean
}

// 미디어 파일 메타
LessonMedia {
  id, lessonNoteId,
  kind: 'audio' | 'photo',
  durationSec?, sizeBytes,
  url, takenAt,
  expiresAt    // = takenAt + retentionDays
}`} />
    </Stack>);

  }

  // ============================================================
  // 14. NOTIF — NotificationTemplate[]
  // ============================================================
  function NotifSection() {
    const [tpls, setTpls] = useState(() => [...D.notificationTemplates]);
    const [policy, setPolicy] = useState(() => ({ ...D.notificationPolicy }));
    const [editingId, setEditingId] = useState(null);
    const editing = tpls.find((t) => t.id === editingId);

    function update(id, patch) {setTpls((ts) => ts.map((t) => t.id === id ? { ...t, ...patch } : t));}

    const triggerLabels = {
      'CHECK_IN': '체크인',
      'CHECK_OUT': '체크아웃',
      'PAYMENT_D-3': '결제 D-3',
      'PAYMENT_DUE': '결제 당일',
      'PAYMENT_OVERDUE': '결제 누락',
      'LESSON_PREREMIND': '수업 전날',
      'JOURNAL_PUBLISHED': '신규 일지',
      'CHANGE_REQ_RESPONSE': '변경 응답',
      'CONSULT_FOLLOWUP': '상담 후속'
    };

    return (
      <Stack>
      <SectionHeader
          title="알림 템플릿"
          entity="NotificationTemplate[]"
          hint="시스템 이벤트마다 발송되는 알림톡·SMS 본문. 토큰 {member.name} 등은 발송 시 치환됨. 카카오 비즈채널 승인 필요." />
        

      <UI.Card>
        <UI.CardHeader title="발송 정책" subtitle="모든 템플릿에 공통 적용" />
        <UI.CardBody>
          <Stack gap={12}>
            <Row>
              <Field label="정숙 시간 (발송 안 함)" hint="이 시간에는 큐에 쌓아두고 다음 날 발송">
                <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                  <input className="input t-mono num" value={policy.quietHours.from} style={{ width: 80 }}
                    onChange={(e) => setPolicy((p) => ({ ...p, quietHours: { ...p.quietHours, from: e.target.value } }))} />
                  <span>–</span>
                  <input className="input t-mono num" value={policy.quietHours.to} style={{ width: 80 }}
                    onChange={(e) => setPolicy((p) => ({ ...p, quietHours: { ...p.quietHours, to: e.target.value } }))} />
                </div>
              </Field>
              <Field label="알림톡 실패 시 대체 채널">
                <select className="select" value={policy.fallbackChannel}
                  onChange={(e) => setPolicy((p) => ({ ...p, fallbackChannel: e.target.value }))}>
                  <option value="sms">SMS 로 자동 전환</option>
                  <option value="none">발송하지 않음</option>
                </select>
              </Field>
              <Field label="회원 1인 일일 캡">
                <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                  <input type="number" className="input t-mono num" value={policy.perDayCap}
                    min={1} max={20} style={{ width: 80 }}
                    onChange={(e) => setPolicy((p) => ({ ...p, perDayCap: parseInt(e.target.value || '0', 10) }))} />
                  <span className="t-caption">건/일</span>
                </div>
              </Field>
            </Row>
            <div className="t-caption" style={{ fontSize: 12 }}>
              발신 채널: <code className="t-mono">{policy.senderProfile}</code>
              <span style={{ marginLeft: 8 }}>(카카오 비즈채널)</span>
            </div>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardBody flush>
          <div className="list-table">
            <div className="list-table__head">
              <span style={{ flex: '0 0 50px' }}>발송</span>
              <span style={{ flex: 1 }}>템플릿</span>
              <span style={{ flex: '0 0 100px' }}>채널</span>
              <span style={{ flex: '0 0 160px' }}>수신 대상</span>
              <span style={{ flex: '0 0 80px', textAlign: 'right' }}></span>
            </div>
            {tpls.map((t) =>
              <div key={t.id} className="list-table__row">
                <div style={{ flex: '0 0 50px' }}>
                  <UI.Toggle checked={t.enabled} onChange={(v) => update(t.id, { enabled: v })} />
                </div>
                <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }}>
                  <div className="row" style={{ gap: 8, alignItems: 'baseline' }}>
                    <span style={{ fontWeight: 600 }}>{t.label}</span>
                    <code className="t-mono" style={{ fontSize: 10.5, color: 'var(--text-tertiary)' }}>{triggerLabels[t.trigger]} · {t.trigger}</code>
                  </div>
                  <span className="t-caption" style={{ fontSize: 11.5, color: 'var(--text-secondary)',
                    overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '52ch' }}>
                    {t.body}
                  </span>
                </div>
                <span style={{ flex: '0 0 100px' }}>
                  <UI.Badge tone={t.channel === 'kakao' ? 'success' : 'neutral'}>
                    {t.channel === 'kakao' ? '알림톡' : 'SMS'}
                  </UI.Badge>
                </span>
                <span style={{ flex: '0 0 160px', fontSize: 12, color: 'var(--text-secondary)' }}>
                  {t.sendTo.map((s) => ({ member: '회원', guardian: '학부모', prospect: '상담자' })[s] || s).join(', ')}
                </span>
                <div style={{ flex: '0 0 80px', display: 'flex', justifyContent: 'flex-end' }}>
                  <UI.Button size="sm" variant="ghost" icon={Icon.Edit} onClick={() => setEditingId(t.id)}>편집</UI.Button>
                </div>
              </div>
              )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Drawer open={!!editing} onClose={() => setEditingId(null)}
        title={editing?.label || ''} width={640}
        footer={<React.Fragment>
          <UI.Button onClick={() => setEditingId(null)}>닫기</UI.Button>
          <UI.Button variant="primary" icon={Icon.Check} onClick={() => setEditingId(null)}>저장</UI.Button>
        </React.Fragment>}>
        {editing &&
          <Stack>
            <Row>
              <Field label="채널">
                <select className="select" value={editing.channel}
                onChange={(e) => update(editing.id, { channel: e.target.value })}>
                  <option value="kakao">알림톡</option>
                  <option value="sms">SMS</option>
                </select>
              </Field>
              <Field label="트리거 (수정 불가)">
                <input className="input t-mono" value={editing.trigger} disabled />
              </Field>
            </Row>

            <Field label="본문" hint="중괄호 토큰은 발송 시 치환됩니다">
              <textarea className="textarea" rows={5} value={editing.body}
              onChange={(e) => update(editing.id, { body: e.target.value })} />
            </Field>

            <div style={{ padding: 12, background: 'var(--bg-canvas)', border: '1px solid var(--border-default)', borderRadius: 6 }}>
              <span className="t-micro">사용 가능한 토큰</span>
              <div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                {[
                '{member.name}', '{teacher.name}', '{academy.name}',
                '{room.singular}', '{room.name}', '{time}', '{date}',
                '{payment.amount}', '{payment.daysOverdue}', '{pass.name}',
                '{decision}'].
                map((tk) =>
                <code key={tk} className="t-mono" style={{
                  padding: '3px 8px', fontSize: 11,
                  background: 'var(--bg-surface)', border: '1px solid var(--border-default)',
                  borderRadius: 4, cursor: 'pointer'
                }}
                onClick={() => update(editing.id, { body: editing.body + ' ' + tk })}>
                    {tk}
                  </code>
                )}
              </div>
            </div>

            <Field label="수신 대상">
              <div className="col" style={{ gap: 6 }}>
                {[
                ['member', '회원 본인'],
                ['guardian', '학부모'],
                ['prospect', '상담자 (가입 전)']].
                map(([v, l]) =>
                <UI.Check key={v} label={l} checked={editing.sendTo.includes(v)}
                onChange={(on) => {
                  const next = on ?
                  [...new Set([...editing.sendTo, v])] :
                  editing.sendTo.filter((x) => x !== v);
                  update(editing.id, { sendTo: next });
                }} />
                )}
              </div>
            </Field>
          </Stack>
          }
      </UI.Drawer>

      <SchemaPeek
          name="NotificationTemplate[] · Org.notificationPolicy"
          schema={`NotificationTemplate {
  id, trigger:
    'CHECK_IN' | 'CHECK_OUT' | 'PAYMENT_D-3' | 'PAYMENT_DUE' |
    'PAYMENT_OVERDUE' | 'LESSON_PREREMIND' | 'JOURNAL_PUBLISHED' |
    'CHANGE_REQ_RESPONSE' | 'CONSULT_FOLLOWUP',
  label, enabled, channel: 'kakao' | 'sms',
  sendTo: ('member' | 'guardian' | 'prospect')[],
  body: string,    // {token} 들이 치환됨
  minimumIntervalMin?: int
}

Org.notificationPolicy {
  quietHours: { from, to },
  fallbackChannel: 'sms' | 'none',
  senderProfile: string,  // 카카오 비즈채널 ID
  perDayCap: int
}

// 발송된 알림은 로그로 남음
NotificationLog {
  id, templateId, memberId, guardianId?,
  sentAt, status: 'sent'|'failed'|'queued',
  channel, renderedBody, errorReason?
}`} />
    </Stack>);

  }

  // ============================================================
  // 15. PAYMENT — Org.paymentPolicy
  // ============================================================
  function PaymentPolicySection() {
    const [p, setP] = useState(() => ({ ...D.paymentPolicy }));
    const upd = (k, v) => setP((prev) => ({ ...prev, [k]: v }));

    return (
      <Stack>
      <SectionHeader
          title="결제 정책"
          entity="Org.paymentPolicy"
          hint="결제 수단·계좌·자동 리마인드 스케줄·환불 규칙. 알림 본문은 '알림 템플릿' 섹션에서 별도 편집." />
        

      <UI.Card>
        <UI.CardHeader title="결제 수단" />
        <UI.CardBody>
          <div className="col" style={{ gap: 10 }}>
            {[
              ['bank', '계좌이체', '학원장이 입금 확인 후 수동 처리'],
              ['card-offline', '오프라인 카드', '학원에 비치된 카드 단말기로 결제'],
              ['card-online', '온라인 카드 (PG)', '회원 모바일에서 직접 결제 — PG 연동 필요'],
              ['cash', '현금', '대면 결제만']].
              map(([v, l, sub]) =>
              <label key={v} className="row" style={{
                gap: 10, padding: 10, alignItems: 'center',
                background: p.methods.includes(v) ? 'var(--accent-subtle-bg)' : 'var(--bg-canvas)',
                border: '1px solid var(--border-default)', borderRadius: 6
              }}>
                <input type="checkbox" checked={p.methods.includes(v)}
                onChange={(e) => upd('methods', e.target.checked ?
                [...p.methods, v] :
                p.methods.filter((x) => x !== v))} />
                <div className="col" style={{ gap: 1, flex: 1 }}>
                  <span style={{ fontWeight: 600, fontSize: 13 }}>{l}</span>
                  <span className="t-caption" style={{ fontSize: 11.5 }}>{sub}</span>
                </div>
                {v === 'card-online' && <UI.Badge tone="neutral">설정 필요</UI.Badge>}
              </label>
              )}
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="입금 계좌" subtitle="회원 결제 안내에 표시됨" />
        <UI.CardBody>
          <Row>
            <Field label="은행">
              <select className="select" value={p.bankAccount.bank}
                onChange={(e) => upd('bankAccount', { ...p.bankAccount, bank: e.target.value })}>
                {['신한', '국민', '하나', '우리', '농협', '카카오뱅크', '토스뱅크'].map((b) => <option key={b}>{b}</option>)}
              </select>
            </Field>
            <Field label="계좌번호" width="240px">
              <input className="input t-mono num" value={p.bankAccount.number}
                onChange={(e) => upd('bankAccount', { ...p.bankAccount, number: e.target.value })} />
            </Field>
            <Field label="예금주">
              <input className="input" value={p.bankAccount.holder}
                onChange={(e) => upd('bankAccount', { ...p.bankAccount, holder: e.target.value })} />
            </Field>
          </Row>
          <div style={{ marginTop: 12 }}>
            <UI.Toggle checked={p.autoMarkPaid} onChange={(v) => upd('autoMarkPaid', v)}
              label="입금 자동 매칭"
              hint="제휴 은행 API 와 연동되면 입금 즉시 결제 완료 처리. (현재 베타)" />
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="자동 리마인드 스케줄" subtitle="결제 임박·누락 시 알림 발송 시점 — 채널·시간 모두 직접 지정"
          actions={<UI.Button size="sm" icon={Icon.Plus} variant="primary" onClick={() => {
            const newId = 'r' + Date.now();
            const arr = Array.isArray(p.reminderSchedule) ? p.reminderSchedule : [];
            upd('reminderSchedule', [...arr, { id: newId, offsetDays: 0, time: '09:00', channel: 'kakao', enabled: true, label: '' }]);
            window.toast && window.toast('새 리마인드 추가 — 발송 시점을 설정하세요');
          }}>리마인드 추가</UI.Button>}
        />
        <UI.CardBody>
          <ReminderBuilder
            items={Array.isArray(p.reminderSchedule) ? p.reminderSchedule : []}
            onChange={(arr) => upd('reminderSchedule', arr)}
          />
          <div className="t-caption" style={{ marginTop: 12, fontSize: 12, lineHeight: 1.7 }}>
            ※ 본문 카피는 <strong>알림 템플릿 → 결제 임박 / 결제 누락</strong> 에서 편집. 정숙시간 22:00–08:00 자동 회피.
          </div>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="증빙 · 환불" />
        <UI.CardBody>
          <Stack gap={12}>
            <Row>
              <Field label="현금영수증">
                <UI.Toggle checked={p.cashReceiptDefault} onChange={(v) => upd('cashReceiptDefault', v)}
                  label="결제 시 기본 발급"
                  hint="회원이 거부할 수 있음. 자진발급 처리" />
              </Field>
              <Field label="세금계산서">
                <UI.Toggle checked={p.taxInvoiceDefault} onChange={(v) => upd('taxInvoiceDefault', v)}
                  label="기본 발급 (사업자 회원)"
                  hint="회원이 사업자등록번호 입력 시 발급" />
              </Field>
            </Row>
            <Field label="환불 정책">
              <select className="select" value={p.refundPolicy}
                onChange={(e) => upd('refundPolicy', e.target.value)}>
                <option value="none">환불 없음</option>
                <option value="prorate">사용한 회차 차감 후 환불 · 권장</option>
                <option value="manual">건별 협의</option>
              </select>
            </Field>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.paymentPolicy"
          schema={`paymentPolicy: {
  methods: ('bank' | 'card-offline' | 'card-online' | 'cash')[],
  bankAccount: { bank, number, holder },
  reminderSchedule: [
    { id, offsetDays: int,        // 결제일 기준 (-N = 전, 0 = 당일, +N = 후)
      time: 'HH:MM',
      channel: 'kakao' | 'sms',
      enabled: boolean,
      label?: string,             // 메모
    }
  ],
  autoMarkPaid: boolean,
  cashReceiptDefault: boolean,
  taxInvoiceDefault: boolean,
  refundPolicy: 'none' | 'prorate' | 'manual'
}

// Payment 도메인 (read model)
Payment {
  id, memberId, planId,
  amount, dueDate, paidAt?,
  status: 'PENDING' | 'DUE_SOON' | 'OVERDUE' | 'PAID' | 'REFUNDED',
  method, lastReminder?,
  receiptIssued: boolean,
  policySnapshot
}`} />
    </Stack>);

  }

  // ============================================================
  // 16. TOKEN POLICY — Org.tokenPolicy
  // ============================================================
  function TokenPolicySection() {
    const [t, setT] = useState(() => ({ ...D.tokenPolicy }));
    const upd = (k, v) => setT((p) => ({ ...p, [k]: v }));

    return (
      <Stack>
      <SectionHeader
          title="접근 링크 정책"
          entity="Org.tokenPolicy"
          hint="회원·학부모는 비밀번호 없이 한 번 본인 확인 후 30일 세션을 사용한다. 이 값들이 그 동작을 정한다." />
        

      <UI.Card>
        <UI.CardHeader title="세션" />
        <UI.CardBody>
          <Stack gap={12}>
            <Row>
              <Field label="본인 확인 후 세션 유효 기간">
                <select className="select" value={t.sessionValidDays}
                  onChange={(e) => upd('sessionValidDays', parseInt(e.target.value, 10))}>
                  <option value={7}>7일 (가장 보수적)</option>
                  <option value={30}>30일 · 권장</option>
                  <option value={90}>90일 (편의 우선)</option>
                </select>
              </Field>
              <Field label="URL 패턴">
                <input className="input t-mono" value={t.urlPattern}
                  onChange={(e) => upd('urlPattern', e.target.value)} />
              </Field>
            </Row>

            <Field label="본인 확인 시 검증 필드">
              <div className="col" style={{ gap: 6 }}>
                {[
                  ['name', '이름 (회원 입력값과 정확히 일치)'],
                  ['birth', '생년월일 (YYYY-MM-DD)'],
                  ['phone', '전화번호 뒷 4자리']].
                  map(([v, l]) =>
                  <UI.Check key={v} label={l} checked={t.identityFields.includes(v)}
                  onChange={(on) => upd('identityFields', on ?
                  [...new Set([...t.identityFields, v])] :
                  t.identityFields.filter((x) => x !== v))} />
                  )}
              </div>
            </Field>
          </Stack>
        </UI.CardBody>
      </UI.Card>

      <UI.Card>
        <UI.CardHeader title="보안 한도" />
        <UI.CardBody>
          <Row>
            <Field label="시간당 시도 한도">
              <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                <input type="number" className="input t-mono num" value={t.rateLimit.perHour}
                  min={1} max={30} style={{ width: 80 }}
                  onChange={(e) => upd('rateLimit', { ...t.rateLimit, perHour: parseInt(e.target.value || '0', 10) })} />
                <span className="t-caption">회/시</span>
              </div>
            </Field>
            <Field label="일일 시도 한도">
              <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                <input type="number" className="input t-mono num" value={t.rateLimit.perDay}
                  min={1} max={100} style={{ width: 80 }}
                  onChange={(e) => upd('rateLimit', { ...t.rateLimit, perDay: parseInt(e.target.value || '0', 10) })} />
                <span className="t-caption">회/일</span>
              </div>
            </Field>
            <Field label="연속 실패 시 잠금">
              <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                <input type="number" className="input t-mono num" value={t.failureLockAfter}
                  min={3} max={20} style={{ width: 80 }}
                  onChange={(e) => upd('failureLockAfter', parseInt(e.target.value || '0', 10))} />
                <span className="t-caption">회 실패</span>
              </div>
            </Field>
          </Row>
          <div style={{ marginTop: 12 }}>
            <UI.Toggle checked={t.guardianAllowed} onChange={(v) => upd('guardianAllowed', v)}
              label="학부모용 별도 토큰 발급"
              hint="회원 토큰과 분리된 학부모 전용 토큰. 권한이 더 좁음 (결제·등하원 알림만, 일지 기본 비공개)" />
          </div>
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Org.tokenPolicy · SecureAccessToken"
          schema={`tokenPolicy: {
  sessionValidDays: 7 | 30 | 90,
  urlPattern: string,
  identityFields: ('name'|'birth'|'phone')[],
  guardianAllowed: boolean,
  rateLimit: { perHour, perDay: int },
  failureLockAfter: int
}

SecureAccessToken {
  id, kind: 'member' | 'guardian',
  memberId | guardianId,
  issuedAt, expiresAt?,
  lastSentAt, lastVerifiedAt,
  failureCount,
  status: 'ACTIVE' | 'PAUSED' | 'REVOKED'
}`}
          notes="토큰은 영구. 본인 확인이 통과되면 그때부터 sessionValidDays 동안 인증된 세션이 발급된다." />
    </Stack>);

  }

  // ============================================================
  // 17. CAMPAIGN — Campaign[] (CampaignCodeManager 재사용)
  // ============================================================
  function CampaignSection({ spaceLabel }) {
    const Manager = AP2.CampaignCodeManager;
    return (
      <Stack>
      <SectionHeader
          title="캠페인 코드"
          entity="Campaign[]"
          hint="광고 링크의 utm_campaign 값을 학원장이 직접 관리. 상담 폼에 들어오는 utm 을 이 카탈로그와 매칭해 채널별 ROI 를 집계한다." />
        
      <UI.Card>
        <UI.CardBody>
          {Manager ? <Manager spaceLabel={spaceLabel} /> : <div>Manager not loaded</div>}
        </UI.CardBody>
      </UI.Card>

      <SchemaPeek
          name="Campaign[] · UtmDict"
          schema={`Campaign {
  code: string,         // 'autumn-25' — utm_campaign 값과 동일
  label, desc,
  period: string,       // '2025-09-01 ~ 2025-11-30' | '상시'
  spend: int (KRW),
  status: 'planned' | 'active' | 'ended',
  // derived (집계)
  leads: int, enrolled: int, cac?: int
}

UtmDict {
  src: Record<string, string>,    // 'instagram' → '인스타그램'
  med: Record<string, string>,
  cmp: Record<CampaignCode, Campaign>
}

// 상담 record 의 utm
Consultation.utm: { src, med, cmp, ref }`} />
    </Stack>);

  }

  // ============================================================
  // Override AdminPages2.Settings — 새 Settings 가 기존을 대체
  // ============================================================
  window.AdminPages2 = Object.assign(window.AdminPages2 || {}, { Settings });

  // ============================================================
  // ReminderBuilder — 결제 리마인드 동적 빌더
  // ============================================================
  function ReminderBuilder({ items, onChange }) {
    if (items.length === 0) {
      return (
        <div style={{ padding: '24px 16px', textAlign: 'center', border: '1px dashed var(--border-default)', borderRadius: 8, background: 'var(--bg-canvas)' }}>
          <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 6 }}>아직 등록된 리마인드가 없습니다</div>
          <div style={{ fontSize: 11.5, color: 'var(--text-tertiary)' }}>위 "리마인드 추가" 버튼으로 발송 시점을 만들어 주세요</div>
        </div>
      );
    }
    // 정렬: offsetDays 오름차순 → time 오름차순
    const sorted = [...items].sort((a, b) => {
      if (a.offsetDays !== b.offsetDays) return a.offsetDays - b.offsetDays;
      return (a.time || '').localeCompare(b.time || '');
    });
    const enabledCount = items.filter(i => i.enabled).length;

    const update = (id, patch) => onChange(items.map(i => i.id === id ? { ...i, ...patch } : i));
    const remove = (id) => onChange(items.filter(i => i.id !== id));

    return (
      <div>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 10 }}>
          <span><strong style={{ color: 'var(--text-primary)' }}>{enabledCount}개 활성</strong> · 총 {items.length}개 정의</span>
          <span>회원당 평균 약 {enabledCount}건 / 결제 1건</span>
        </div>

        {/* 시점 시각화 — 결제일 기준 타임라인 */}
        <ReminderTimeline items={sorted}/>

        <div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 8 }}>
          {sorted.map(it => (
            <ReminderRow key={it.id} item={it} onUpdate={(patch) => update(it.id, patch)} onRemove={() => remove(it.id)}/>
          ))}
        </div>
      </div>
    );
  }

  function ReminderRow({ item, onUpdate, onRemove }) {
    const phase = item.offsetDays < 0 ? 'before' : item.offsetDays === 0 ? 'on' : 'after';
    const phaseColor = phase === 'before' ? 'var(--status-warning)' : phase === 'on' ? 'var(--accent-default)' : 'var(--status-danger)';
    const phaseBg    = phase === 'before' ? 'var(--status-warning-bg)' : phase === 'on' ? 'var(--accent-subtle-bg)' : 'var(--status-danger-bg)';

    const setPhase = (next) => {
      if (next === 'on') onUpdate({ offsetDays: 0 });
      else if (next === 'before') onUpdate({ offsetDays: -Math.max(1, Math.abs(item.offsetDays || 1)) });
      else onUpdate({ offsetDays: Math.max(1, Math.abs(item.offsetDays || 1)) });
    };

    return (
      <div className={`rem-row ${item.enabled ? '' : 'is-off'}`}>
        {/* enable toggle */}
        <button className={`rem-toggle ${item.enabled ? 'is-on' : ''}`}
          onClick={() => onUpdate({ enabled: !item.enabled })}
          title={item.enabled ? '리마인드 활성 — 클릭해서 비활성' : '리마인드 비활성 — 클릭해서 활성'}>
          <span/>
        </button>

        {/* phase segmented */}
        <div className="rem-phase-seg" style={{ '--phase-color': phaseColor, '--phase-bg': phaseBg }}>
          <button className={`rem-phase-seg__btn ${phase === 'before' ? 'is-on' : ''}`} onClick={() => setPhase('before')}>전</button>
          <button className={`rem-phase-seg__btn ${phase === 'on' ? 'is-on' : ''}`} onClick={() => setPhase('on')}>당일</button>
          <button className={`rem-phase-seg__btn ${phase === 'after' ? 'is-on' : ''}`} onClick={() => setPhase('after')}>후</button>
        </div>

        {/* days */}
        <div className="rem-days">
          {phase !== 'on' ? (
            <React.Fragment>
              <input type="number" min="1" max="60" className="rem-days__inp" value={Math.abs(item.offsetDays) || 1}
                onChange={(e) => {
                  const v = Math.max(1, parseInt(e.target.value || '1', 10));
                  onUpdate({ offsetDays: phase === 'before' ? -v : v });
                }}/>
              <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>일</span>
            </React.Fragment>
          ) : (
            <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>—</span>
          )}
        </div>

        {/* time */}
        <div className="rem-time">
          <Icon.Clock size={12}/>
          <input type="time" className="rem-time__inp" value={item.time || '09:00'}
            onChange={(e) => onUpdate({ time: e.target.value })}/>
        </div>

        {/* channel */}
        <div className="rem-channel">
          {[
            { id: 'kakao', label: '알림톡' },
            { id: 'sms',   label: 'SMS' },
          ].map(c => (
            <button key={c.id}
              className={`rem-channel__btn ${item.channel === c.id ? 'is-on' : ''}`}
              onClick={() => onUpdate({ channel: c.id })}>
              {c.label}
            </button>
          ))}
        </div>

        {/* label */}
        <input className="rem-label" placeholder="메모 (예: 학부모 동시 발송)" value={item.label || ''}
          onChange={(e) => onUpdate({ label: e.target.value })}/>

        {/* delete */}
        <button className="rem-del" onClick={onRemove} title="삭제"><Icon.X size={14}/></button>
      </div>
    );
  }

  function ReminderTimeline({ items }) {
    // -7일 ~ +14일 범위에 점 찍기
    const minDay = Math.min(-7, ...items.map(i => i.offsetDays));
    const maxDay = Math.max(14, ...items.map(i => i.offsetDays));
    const range = maxDay - minDay;
    const xOf = (d) => ((d - minDay) / range) * 100;
    const dayZero = xOf(0);

    return (
      <div className="rem-timeline">
        <div className="rem-timeline__rail">
          <div className="rem-timeline__zero" style={{ left: `${dayZero}%` }}>
            <span className="rem-timeline__zero-label">결제일</span>
          </div>
          {/* gridlines */}
          {[-7, -3, 0, 3, 7, 14].map(d => {
            const x = xOf(d);
            if (x < 0 || x > 100) return null;
            return (
              <div key={d} className="rem-timeline__tick" style={{ left: `${x}%` }}>
                <span className="rem-timeline__tick-label">{d === 0 ? '' : (d > 0 ? '+' : '') + d + '일'}</span>
              </div>
            );
          })}
          {items.map((it, i) => {
            const x = xOf(it.offsetDays);
            const tone = it.offsetDays < 0 ? 'var(--status-warning)' : it.offsetDays === 0 ? 'var(--accent-default)' : 'var(--status-danger)';
            return (
              <div key={it.id}
                className={`rem-timeline__dot ${it.enabled ? '' : 'is-off'}`}
                style={{ left: `${x}%`, background: it.enabled ? tone : 'var(--text-tertiary)' }}
                title={`${it.offsetDays}일 · ${it.time} · ${it.channel === 'kakao' ? '알림톡' : 'SMS'}`}>
                <span className="rem-timeline__dot-time">{it.time}</span>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

})();