/* ============================================================
   Feedback — 공용 헬퍼(STAGE_*, FEED_CHANNELS, rangeKey, PeriodCalendar 등)는 screens-episode.jsx에서 이동해왔다
   (load order상 screens-episode.jsx 다음에 로드). 피드백 페이지 본체는 WbsFeedbackBoard(WBS 구조).
   ============================================================ */

const FEED_CHANNELS = [['INSTAGRAM','Instagram'],['LINKEDIN','LinkedIn'],['FACEBOOK','Facebook'],['NEWSROOM','Newsroom'],['OTHER','기타']];
const STAGE_LABEL = { PLAN: '기획', DESIGN: '디자인' };

const STAGE_COLOR = { PLAN: '#e8590c', DESIGN: '#1971c2' };  // 기획=주황(난색) · 디자인=파랑(한색)
const STAGE_SOFT = { PLAN: '#fff1e8', DESIGN: '#e7f1fb' };
const STAGE_SHORT = { PLAN: '기', DESIGN: '디' };

// 작성자 역할 색: LGE=빨강, CNX·ADMIN=파랑 (요구 규칙). ROLE_BADGE(screens-episode.jsx) 색을
// 재사용하되 ADMIN도 CNX(파랑)로 통일 — 단일 출처로 팔레트 중복 제거.
const memoRoleStyle = (role) => (role === 'LGE' ? ROLE_BADGE.LGE : ROLE_BADGE.CNX);
const canDeleteNote = (note, user) => (note.author && note.author.id === user.id) || user.role === 'CNX' || user.role === 'ADMIN';

// 공용 메모 카드 — 에피소드별 메모·공통 메모 양쪽에서 재사용. 역할 색 배지 포함.
function MemoNoteCard({ note, canDelete, onDelete }) {
  const role = note.author && note.author.role;
  const rc = memoRoleStyle(role);
  return (
    <div className="card" style={{ padding: '9px 11px' }}>
      <div className="flex between center" style={{ marginBottom: 3 }}>
        <span style={{ fontSize: 12.5, minWidth: 0, overflow: 'hidden' }}>
          <b>{note.author && note.author.name}</b>
          <span className="pill" style={{ marginLeft: 5, fontSize: 9.5, color: rc.c, background: rc.bg }}>{role}</span>
          <span className="muted" style={{ marginLeft: 6, fontSize: 11 }}>{String(note.createdAt).slice(0, 10)}</span>
        </span>
        {canDelete && <button onClick={onDelete} title="삭제" style={{ border: 'none', background: 'transparent', color: 'var(--ink-4)', cursor: 'pointer', fontSize: 13, flexShrink: 0 }}>✕</button>}
      </div>
      <div style={{ fontSize: 13, lineHeight: 1.55, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink-2)' }}>{note.body}</div>
    </div>
  );
}

// 공용 메모 입력기 — 자체 body/busy 상태 보유. onSubmit(text)가 API 호출+목록 갱신을 담당.
// 에피소드별 메모·공통 메모 입력에서 재사용(입력 스캐폴드 중복 제거).
function NoteComposer({ placeholder, onSubmit }) {
  const [body, setBody] = useSE('');
  const [busy, setBusy] = useSE(false);
  const submit = async () => {
    if (!body.trim() || busy) return;
    setBusy(true);
    try { await onSubmit(body.trim()); setBody(''); }
    catch (e) { alert('작성 실패: ' + (e.message || e)); }
    finally { setBusy(false); }
  };
  return (
    <div className="flex" style={{ gap: 6 }}>
      <textarea value={body} onChange={(e) => setBody(e.target.value)} rows={2} placeholder={placeholder}
        style={{ flex: 1, fontSize: 13, padding: '8px 10px', borderRadius: 8, border: '1px solid var(--line)', resize: 'vertical', fontFamily: 'inherit' }} />
      <button onClick={submit} disabled={busy || !body.trim()}
        style={{ background: body.trim() ? '#16181d' : 'var(--line)', color: body.trim() ? '#fff' : 'var(--ink-3)', border: 'none', borderRadius: 8, padding: '0 16px', fontWeight: 700, fontSize: 13, cursor: body.trim() ? 'pointer' : 'default' }}>{busy ? '…' : '등록'}</button>
    </div>
  );
}
const rangeKey = (x) => `${x.stage}|${x.label}|${String(x.startDate).slice(0, 10)}|${String(x.endDate).slice(0, 10)}`;
function PeriodCalendar({ rows, wbsSource, onAdopt, onUnadopt, hoverKey, onHover, onPick, selectedKey }) {
  // 채택된 WBS(=같은 키의 설정 기간 존재)는 막대를 또 만들지 않고 '설정 기간'(채움) 하나로만 표시.
  const periodKeys = new Set((rows || []).filter((r) => r.startDate && r.endDate).map(rangeKey));
  const ranges = [
    ...(rows || []).filter((r) => r.startDate && r.endDate).map((r) => ({ ...r, kind: 'period' })),
    ...(wbsSource || []).filter((s) => s.startDate && s.endDate && !periodKeys.has(rangeKey(s))).map((s) => ({ ...s, kind: 'wbs' })),
  ];
  if (ranges.length === 0) return <div className="muted" style={{ fontSize: 12.5, padding: 12, textAlign: 'center' }}>WBS 불러오기 또는 기간 추가하면 달력에 표시됩니다.</div>;
  const times = ranges.flatMap((x) => [new Date(x.startDate).getTime(), new Date(x.endDate).getTime()]);
  const lo = new Date(Math.min.apply(null, times)); lo.setDate(1);
  const hi = new Date(Math.max.apply(null, times));
  const months = [];
  const d = new Date(lo);
  while (d.getTime() <= hi.getTime()) { months.push(new Date(d)); d.setMonth(d.getMonth() + 1); }
  const dayStart = (v) => new Date(new Date(v).setHours(0, 0, 0, 0)).getTime();
  const dayEnd = (v) => new Date(new Date(v).setHours(23, 59, 59, 999)).getTime();
  const inRange = (day, x) => { const t = day.getTime(); return t >= dayStart(x.startDate) && t <= dayEnd(x.endDate); };
  const sameDay = (a, b) => { const x = new Date(b); return a.getFullYear() === x.getFullYear() && a.getMonth() === x.getMonth() && a.getDate() === x.getDate(); };
  // 전역 레인 배정 — 겹치는 일정끼리 다른 줄, 모든 날에서 같은 일정은 항상 같은 세로 위치(구글 캘린더식).
  const laneEnds = [];
  [...ranges].sort((a, b) => dayStart(a.startDate) - dayStart(b.startDate) || dayEnd(a.endDate) - dayEnd(b.endDate)).forEach((x) => {
    let lane = laneEnds.findIndex((end) => end < dayStart(x.startDate));
    if (lane === -1) lane = laneEnds.length;
    laneEnds[lane] = dayEnd(x.endDate);
    x.lane = lane;
  });
  const laneCount = Math.max(1, laneEnds.length);
  const LANE_H = 13;
  const cellH = 16 + laneCount * (LANE_H + 1);
  return (
    <div>
      <div style={{ display: 'flex', gap: 14, alignItems: 'flex-start', overflowX: 'auto', paddingBottom: 6 }}>
      {months.map((m, mi) => {
        const year = m.getFullYear(), mon = m.getMonth();
        const startDow = new Date(year, mon, 1).getDay();
        const days = new Date(year, mon + 1, 0).getDate();
        const cells = [];
        for (let i = 0; i < startDow; i++) cells.push(null);
        for (let dd = 1; dd <= days; dd++) cells.push(new Date(year, mon, dd));
        return (
          <div key={mi} style={{ flex: '0 0 252px', width: 252 }}>
            <div style={{ fontWeight: 700, fontSize: 12.5, marginBottom: 5 }}>{year}년 {mon + 1}월</div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 2 }}>
              {['일', '월', '화', '수', '목', '금', '토'].map((w) => <div key={w} className="muted" style={{ fontSize: 9.5, textAlign: 'center' }}>{w}</div>)}
              {cells.map((day, ci) => {
                if (!day) return <div key={ci} style={{ height: cellH }} />;
                const covering = ranges.filter((x) => inRange(day, x));
                const weekStart = day.getDay() === 0 || day.getDate() === 1;
                return (
                  <div key={ci}
                    title={covering.map((x) => `${x.kind === 'wbs' ? 'WBS' : '설정 기간'} · ${STAGE_LABEL[x.stage]} ${x.label}${x.task ? ' — ' + x.task : ''} (${x.kind === 'wbs' ? '클릭=채택' : '클릭=채택 취소'})`).join('\n') || undefined}
                    style={{ height: cellH, overflow: 'hidden', border: '1px solid #eef0f2', borderRadius: 4, paddingTop: 1, background: '#fff' }}>
                    <div style={{ fontSize: 10, fontWeight: 600, color: day.getDay() === 0 ? '#C8102E' : '#888', paddingLeft: 3, lineHeight: '14px' }}>{day.getDate()}</div>
                    {Array.from({ length: laneCount }).map((_, lane) => {
                      const x = covering.find((r) => r.lane === lane);
                      if (!x) return <div key={lane} style={{ height: LANE_H, marginTop: 1 }} />;
                      const col = STAGE_COLOR[x.stage];
                      const isPeriod = x.kind === 'period';
                      const isStart = sameDay(day, x.startDate);
                      const isEnd = sameDay(day, x.endDate);
                      const showLabel = isStart || weekStart;
                      const k = rangeKey(x);
                      const sel = selectedKey && selectedKey === k;
                      const hot = (hoverKey && hoverKey === k) || sel;
                      const dim = (hoverKey || selectedKey) && !hot;
                      const onBar = onPick ? () => onPick(x) : (isPeriod ? (onUnadopt ? () => onUnadopt(x) : undefined) : () => onAdopt(x));
                      return (
                        <div key={lane}
                          onClick={onBar}
                          onMouseEnter={onHover ? () => onHover(x) : undefined}
                          onMouseLeave={onHover ? () => onHover(null) : undefined}
                          style={{ height: LANE_H, marginTop: 1, fontSize: 8.5, lineHeight: `${LANE_H}px`, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'clip', cursor: onBar ? 'pointer' : 'default',
                          color: (isPeriod || hot) ? '#fff' : col, background: hot ? col : (isPeriod ? col : STAGE_SOFT[x.stage]),
                          opacity: dim ? 0.3 : 1, boxShadow: sel ? 'inset 0 0 0 1.5px #16181d' : undefined,
                          borderTop: `1px solid ${col}`, borderBottom: `1px solid ${col}`,
                          borderLeft: isStart ? `1px solid ${col}` : 'none', borderRight: isEnd ? `1px solid ${col}` : 'none',
                          borderTopLeftRadius: isStart ? 3 : 0, borderBottomLeftRadius: isStart ? 3 : 0,
                          borderTopRightRadius: isEnd ? 3 : 0, borderBottomRightRadius: isEnd ? 3 : 0,
                          paddingLeft: showLabel ? 3 : 0 }}>
                        {showLabel ? `${STAGE_SHORT[x.stage]} ${x.label || x.task}` : ''}
                      </div>
                      );
                    })}
                  </div>
                );
              })}
            </div>
          </div>
        );
      })}
      </div>
      <div className="flex" style={{ gap: 14, marginTop: 10, fontSize: 11, flexWrap: 'wrap', alignItems: 'center' }}>
        <span className="flex" style={{ gap: 5, alignItems: 'center' }}><span style={{ width: 22, height: 10, borderRadius: 3, background: STAGE_COLOR.PLAN }} />기획</span>
        <span className="flex" style={{ gap: 5, alignItems: 'center' }}><span style={{ width: 22, height: 10, borderRadius: 3, background: STAGE_COLOR.DESIGN }} />디자인</span>
        <span className="muted">· 채움 = 설정된 기간, 연한 테두리 = WBS 윈도우(클릭=채택)</span>
      </div>
    </div>
  );
}

/* 선택된 에피소드 1개의 상세 피드백 — EpisodeNote 목록 + 입력. 빈-문구 미표시. */
function EpisodeFeedbackDetail({ ep, user }) {
  const [notes, setNotes] = useSE(null);
  const reload = () => window.api.getEpisodeNotes(ep.id).then((d) => setNotes(d.notes || [])).catch(() => setNotes([]));
  useEE(() => { reload(); }, [ep.id]);
  const del = async (noteId) => {
    if (!confirm('이 메모를 삭제할까요?')) return;
    try { await window.api.deleteEpisodeNote(noteId); await reload(); }
    catch (e) { alert('삭제 실패: ' + (e.message || e)); }
  };
  return (
    <div className="card" style={{ padding: 14, marginTop: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
      <div style={{ fontWeight: 750, fontSize: 15, letterSpacing: '-.01em', minWidth: 0, wordBreak: 'break-word' }}>
        {ep.title}
        {ep.personaLabel && <span style={{ marginLeft: 8, fontSize: 12, fontWeight: 650, color: 'var(--cnx-blue)' }}>· {ep.personaLabel}</span>}
      </div>
      {notes && notes.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {notes.map((n) => (
            <MemoNoteCard key={n.id} note={n} canDelete={canDeleteNote(n, user)} onDelete={() => del(n.id)} />
          ))}
        </div>
      )}
      <NoteComposer placeholder="이 에피소드에 메모…"
        onSubmit={async (text) => { await window.api.addEpisodeNote(ep.id, text); await reload(); }} />
    </div>
  );
}

/* '테마 및 에피소드 선정' 상세 — 고객사가 최종 선택한 에피소드(최신 제출)를 카드로 노출.
   프로젝트↔WBS 사이클은 직접 링크가 없어 액션 task의 "[8-9월]" 숫자와 프로젝트 이름의
   월 숫자 교집합으로 매핑한다(불일치 시 active 프로젝트로 폴백). */
// 문자열에서 월 숫자(1~12)만 추출 — 콘텐츠 프로젝트명 ↔ WBS 사이클명 매칭에 쓴다.
const monthNums = (s) => (String(s || '').match(/\d+/g) || []).map(Number).filter((n) => n >= 1 && n <= 12);
// items 중 이름의 월 숫자가 wantNums와 가장 많이 겹치는(>0) 항목을 고른다. 겹침이 없으면 null.
const bestMonthMatch = (items, wantNums, nameOf) => {
  if (!wantNums.length) return null;
  const scored = items.map((x) => ({ x, hit: monthNums(nameOf(x)).filter((n) => wantNums.includes(n)).length })).sort((a, b) => b.hit - a.hit);
  return scored[0] && scored[0].hit > 0 ? scored[0].x : null;
};

function SelectedEpisodeMemos({ actSel, cyc, user, month, focusEpisodeId }) {
  const [state, setState] = useSE({ loading: true, eps: [] });
  const [selectedId, setSelectedId] = useSE(null);
  useEE(() => {
    let alive = true;
    (async () => {
      try {
        const projects = await window.api.getProjects();
        const wantNums = monthNums(actSel.task).length ? monthNums(actSel.task) : monthNums(cyc && cyc.name);
        const proj = bestMonthMatch(projects, wantNums, (p) => p.name)
          || projects.find((p) => p.status === 'active') || projects[0];
        if (!proj) { if (alive) setState({ loading: false, eps: [] }); return; }
        const data = await window.api.getContent(proj.id);
        // 에피소드의 발행 월(ContentMonth) — 테마의 monthId로 유도. 라벨 포맷이 사이클마다 다를 수
        // 있어 숫자(1~12)로 정규화(monthNums)해 둔다. 피드백 월 탭 분리에 쓴다.
        const monthNumById = {};
        (data.months || []).forEach((m) => { monthNumById[m.id] = monthNums(m.label)[0] || null; });
        const byId = {};
        (data.themes || []).forEach((t) => (t.episodes || []).forEach((e) => { byId[e.id] = { ...e, monthNum: monthNumById[t.monthId] || null }; }));
        const eps = (data.currentSelection || [])
          .map((s) => (byId[s.episodeId] ? { ...byId[s.episodeId], personaLabel: s.personaLabel } : null))
          .filter(Boolean);
        if (alive) setState({ loading: false, eps });
      } catch { if (alive) setState({ loading: false, eps: [] }); }
    })();
    return () => { alive = false; };
  }, [actSel.id]);
  const dlEpDone = React.useRef(false);
  useEE(() => {
    if (!focusEpisodeId || dlEpDone.current || state.loading) return;
    if (state.eps.some((e) => e.id === focusEpisodeId)) { setSelectedId(focusEpisodeId); dlEpDone.current = true; }
  }, [state]);
  if (state.loading) return <div className="muted" style={{ fontSize: 12.5, marginBottom: 14 }}>선정된 에피소드 불러오는 중…</div>;
  // 현재 피드백 월 탭(month)의 숫자와 일치하는 에피소드만 노출 — 라벨 문자열이 아닌 월 숫자로 매칭.
  // 단, 월을 알 수 없는 에피소드(monthNum null: 테마 monthId 없음/라벨에 숫자 없음)는 어느 탭에서도
  // 숨기지 않고 항상 노출한다 — 월 분리 때문에 선정 에피소드가 사라지는 회귀 방지.
  const wantMonth = month ? (monthNums(month)[0] || null) : null;
  const eps = wantMonth == null ? state.eps : state.eps.filter((e) => e.monthNum == null || e.monthNum === wantMonth);
  const sel = eps.find((e) => e.id === selectedId) || state.eps.find((e) => e.id === selectedId) || null;
  return (
    <div style={{ marginBottom: 16 }}>
      <div className="muted" style={{ fontSize: 12, fontWeight: 700, margin: '0 0 8px' }}>선택된 에피소드에 대한 내용을 입력해주세요</div>
      {eps.length === 0 && !sel
        ? <div className="muted" style={{ fontSize: 12.5 }}>선택된 에피소드가 없습니다</div>
        : (
          <div>
            <div className="ep-grid">
              {eps.map((ep) => {
                const pick = () => setSelectedId((cur) => (cur === ep.id ? null : ep.id));
                return (
                  <EpisodeCard key={ep.id} ep={ep} selected={ep.id === selectedId}
                    personaLabel={ep.personaLabel} isStaff={false} onToggle={pick} onOpen={pick} bare />
                );
              })}
            </div>
            {sel
              ? <EpisodeFeedbackDetail key={sel.id} ep={sel} user={user} />
              : <div className="muted" style={{ fontSize: 12, marginTop: 10 }}>위에서 에피소드를 선택하세요.</div>}
          </div>
        )}
    </div>
  );
}

/* ---------------- Feedback board v2 (WBS 구조: 프로젝트 → 검토·피드백 액션 → 메모) ---------------- */
function WbsFeedbackBoard({ user, navigate, route }) {
  const [ov, setOv] = useSE(null);
  const [cycId, setCycId] = useSE(null);
  const [actSel, setActSel] = useSE(null);
  const [notes, setNotes] = useSE(null);
  const [listOpen, setListOpen] = useSE(true);  // 상단 검토 항목 목록 접기(네이버 블로그식) → 문서 영역에 집중
  const [monKey, setMonKey] = useSE(null);      // L2(월) 탭 — null이면 첫 월
  const [dlDoc, setDlDoc] = useSE(null);        // 딥링크 문서 정보 { tab, docId, page, annoId }
  const [dlEpisode, setDlEpisode] = useSE(null);
  const reloadOv = () => window.api.getOverview().then(setOv).catch(() => setOv({ cycles: [] }));
  useEE(() => { reloadOv(); }, []);
  const cycles = (ov && ov.cycles) || [];
  const cyc = cycles.find((c) => c.id === cycId) || cycles[0] || null;
  const isFb = (a) => window.api.isFeedbackTask(a.task);
  // 문서(업로드·주석) 화면으로 열지, 메모 화면으로 열지. 개별 지정 작업도 문서 화면으로 연다
  // — 목록에만 뜨고 상세에선 업로드를 못 하면 반쪽이라.
  const isDoc = (a) => (a.task || '').includes('검토·피드백') || window.api.isPinnedFeedbackTask(a.task);
  // 메모 입력 등 무관한 렌더마다 액션 전체를 복제하지 않도록 메모이즈.
  const groups = useME(() => {
    if (!cyc) return [];
    // 공유 스텝(콘텐츠 플래닝 등)은 여러 월에 복제된다 → Overview와 동일하게 각 월 탭에 노출한다.
    // dedup은 월 내에서만(같은 액션이 한 월에 두 번 뜨는 것 방지). stepLabel = L3(단계) 배지용.
    // 주차(월 탭)는 그 주차 전용 액션(LGE 검토·피드백 + CNX 기획/디자인 + 발행)이 "모두" 완료돼야 숨긴다.
    // LGE만 완료돼도 CNX가 남아있으면 계속 노출 (07.24 피드백). 공유 플래닝은 주차 전용이 아니라 판정 제외.
    return cyc.months
      .filter((m) => {
        const wk = m.steps.filter((s) => !s.shared).flatMap((s) => s.actions);
        return !(wk.length > 0 && wk.every((a) => a.completed));
      })
      .map((m) => {
        const seen = new Set();
        return {
          month: m.month,
          // 주차가 보이는 동안엔 완료 여부와 무관하게 그 주차의 검토·피드백 항목을 모두 노출.
          // 단 공유 단계(콘텐츠 플래닝 등)는 제외 — 모든 월 탭에 똑같이 복제돼 목록만 어지럽힌다.
          actions: m.steps.filter((s) => !s.shared)
            .flatMap((s) => s.actions.filter((a) => isFb(a)).map((a) => ({ ...a, stepLabel: s.label })))
            .filter((a) => (seen.has(a.id) ? false : (seen.add(a.id), true))),
        };
      }).filter((g) => g.actions.length > 0);
  }, [ov, cycId]);
  const grp = groups.find((g) => g.month === monKey) || groups[0] || null;
  const isEpisodeSel = !!actSel && (actSel.task || '').includes('테마 및 에피소드 선정');
  const loadNotes = (a) => { setActSel(a); setNotes(null); window.api.getActionNotes(a.id).then(setNotes).catch(() => setNotes([])); };

  // 기본 사이클 선택(1회) — 늘 첫 사이클(cycles[0])로 떨어지던 것을, 검토·피드백이 남아있는
  // 첫 사이클로 바꾼다(운영일정 Overview의 firstOpenCycle과 같은 취지). 딥링크가 있으면
  // 아래 딥링크 effect가 대상을 정하므로 여기선 건너뛴다.
  const autoCyclePicked = React.useRef(false);
  useEE(() => {
    if (autoCyclePicked.current || cycId != null || !ov) return;
    if (route && (route.selectActionId || route.selectTask || route.cycleMatch || route.focusEpisodeId)) return;
    if (!cycles.length) return;
    autoCyclePicked.current = true;
    const hasPending = (c) => (c.months || []).some((m) => (m.steps || []).some((s) => (s.actions || []).some((a) => isFb(a) && !a.completed)));
    const target = cycles.find(hasPending) || cycles[0];
    if (target) setCycId(target.id);
  }, [ov]);

  // 딥링크(1회 소비): 타깃 사이클을 cycles에서 동기 해석한 뒤 그 사이클 안에서 액션을 찾아
  // cycId·monKey·actSel을 한 번에 세팅한다. cycId 상태에 의존하지 않아 같은 flush 레이스가 없고,
  // dlDone ref로 ov 재로드(메모 추가/삭제)·수동 사이클 이동에 재발동하지 않는다.
  const dlDone = React.useRef(false);
  useEE(() => {
    if (!ov || dlDone.current) return;
    if (!(route && (route.selectActionId || route.selectTask || route.cycleMatch || route.focusEpisodeId))) return;
    if (route.docTab || route.docId) setDlDoc({ tab: route.docTab, docId: route.docId, page: route.page, annoId: route.annoId });

    // 1) 타깃 사이클을 cycles에서 동기 해석(상태 cycId에 의존하지 않음)
    let target = null;
    if (route.selectActionId)
      target = cycles.find((c) => (c.months || []).some((m) => (m.steps || []).some((s) => (s.actions || []).some((a) => a.id === route.selectActionId))));
    if (!target && route.cycleMatch)
      target = bestMonthMatch(cycles, monthNums(route.cycleMatch), (c) => c.name);
    if (!target) target = cyc;                    // 폴백: 현재/첫 사이클
    if (!target) return;                          // 사이클이 아예 없음

    // 2) 그 사이클 안에서 첫 매칭 액션 찾기(피드백 목록과 동일 필터: isFb — 완료 항목도 노출되므로 완료 제외 안 함)
    const match = (a) => route.selectActionId ? a.id === route.selectActionId : (route.selectTask ? (a.task || '').includes(route.selectTask) : false);
    let hitMonth = null, hitAct = null;
    for (const m of (target.months || [])) {
      const hit = (m.steps || []).flatMap((s) => s.actions || []).find((a) => isFb(a) && match(a));
      if (hit) { hitMonth = m.month; hitAct = hit; break; }
    }

    if (route.focusEpisodeId && !hitAct) {
      for (const m of (target.months || [])) {
        const hit = (m.steps || []).flatMap((s) => s.actions || []).find((a) => (a.task || '').includes('테마 및 에피소드 선정'));
        if (hit) { hitMonth = m.month; hitAct = hit; break; }
      }
      if (hitAct) setDlEpisode(route.focusEpisodeId);
    }

    dlDone.current = true;                         // 소비 완료
    if (target.id !== cycId) setCycId(target.id);
    if (hitAct) { setMonKey(hitMonth); loadNotes(hitAct); }
  }, [ov]);
  const del = async (noteId) => {
    if (!confirm('이 메모를 삭제할까요?')) return;
    try { await window.api.deleteActionNote(actSel.id, noteId); setNotes(await window.api.getActionNotes(actSel.id)); reloadOv(); }
    catch (e) { alert('삭제 실패: ' + (e.message || e)); }
  };
  const noteCard = (n) => (
    <MemoNoteCard key={n.id} note={n} canDelete={canDeleteNote(n, user)} onDelete={() => del(n.id)} />
  );
  return (
    <div className="page-wrap">
      <h2 className="page-title" style={{ margin: '0 0 20px' }}>피드백</h2>
      {/* L1(사이클)·L2(월)은 운영일정 Overview와 동일한 세그먼트 탭(ov-cyclebar / seg) */}
      <div className="ov-cyclebar">
        {cycles.map((c) => (
          <button key={c.id} className={'ov-cyclebtn' + (cyc && cyc.id === c.id ? ' on' : '')}
            onClick={() => { setCycId(c.id); setMonKey(null); setActSel(null); setNotes(null); }}>{c.name}</button>
        ))}
      </div>
      {!ov ? <div className="muted" style={{ padding: 16 }}>불러오는 중…</div>
        : groups.length === 0 ? <div className="muted" style={{ padding: 24, textAlign: 'center' }}>검토·피드백 항목이 없습니다.</div>
        : (
        <div>
          <div className="seg subtab-seg" style={{ marginBottom: 18 }}>
            {groups.map((g) => (
              <button key={g.month} className={g.month === grp.month ? 'on red' : ''}
                onClick={() => { if (g.month === grp.month) return; setMonKey(g.month); setActSel(null); setNotes(null); setDlDoc(null); }}>{g.month}</button>
            ))}
          </div>
          {/* 상단 검토 항목 목록(네이버 블로그식 표) — L3(단계) 배지 + L4(항목), 접기 가능 */}
          <div className="card fb-list">
            <button className="fb-list-head" onClick={() => setListOpen(!listOpen)} aria-expanded={listOpen}>
              <b>검토 항목</b><span className="fb-count">{grp.actions.length}</span>
              <span className="fb-fold">{listOpen ? '목록 닫기 ▲' : '목록 열기 ▼'}</span>
            </button>
            {listOpen && grp.actions.map((a) => {
              const on = actSel && actSel.id === a.id;
              // 손으로 고른 항목 — 딥링크를 비운다. 안 그러면 이전 알림의 docTab이 이 항목의 기본 탭을 덮어쓴다
              return (
                <div key={a.id} className={'fb-row' + (on ? ' sel' : '')} onClick={() => { setDlDoc(null); loadNotes(a); }}>
                  <span className="tag fb-step">{a.stepLabel}</span>
                  <span className="fb-task" title={a.task}>{a.task}</span>
                  {/* 운영일정에서 완료 처리된 업무는 여기서도 '완료'로 표시(Overview의 완료 배지와 같은 색) */}
                  {a.completed && <span className="fb-done">완료</span>}
                  {a.noteCount > 0 && <span className="fb-meta">메모 {a.noteCount}</span>}
                </div>
              );
            })}
          </div>
          <div className="card fb-detail">
            {!actSel ? <div className="muted" style={{ padding: 16 }}>위 <b>검토 항목</b>에서 문서를 선택하세요.</div>
              : isDoc(actSel) ? (
                <div>
                  {notes && notes.length > 0 && (
                    <div style={{ marginBottom: 14 }}>
                      <div className="muted" style={{ fontSize: 12, fontWeight: 700, margin: '0 0 6px' }}>이전 메모 (문서주석 도입 전)</div>
                      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{notes.map(noteCard)}</div>
                    </div>
                  )}
                  <DocReviewTabs key={actSel.id} action={actSel} user={user}
                    initialTab={dlDoc && dlDoc.docId ? dlDoc.tab : undefined}
                    initialDocId={dlDoc ? dlDoc.docId : undefined}
                    initialPage={dlDoc ? dlDoc.page : undefined}
                    initialAnnoId={dlDoc ? dlDoc.annoId : undefined} />
                </div>
              )
              : (
                <div>
                  <div style={{ fontWeight: 750, fontSize: 18, letterSpacing: '-.01em', marginBottom: 14 }}>{actSel.task}</div>
                  {/* 전체 피드백(공통) — 에피소드 카드 위에 배치 */}
                  {isEpisodeSel && <div className="muted" style={{ fontSize: 12, fontWeight: 700, margin: '0 0 8px' }}>전체 피드백</div>}
                  {notes === null ? <div className="muted" style={{ fontSize: 12.5 }}>불러오는 중…</div>
                    : notes.length === 0 ? null
                    : <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 12 }}>
                        {notes.map(noteCard)}
                      </div>}
                  <NoteComposer placeholder="공통으로 남길 피드백은 해당 칸에서 진행해주세요"
                    onSubmit={async (text) => { await window.api.addActionNote(actSel.id, text); setNotes(await window.api.getActionNotes(actSel.id)); reloadOv(); }} />
                  {isEpisodeSel && <div style={{ marginTop: 20, paddingTop: 18, borderTop: '1px solid var(--line)' }}>
                    <SelectedEpisodeMemos key={`${actSel.id}-${grp.month}`} actSel={actSel} cyc={cyc} user={user} month={grp.month} focusEpisodeId={dlEpisode} />
                  </div>}
                </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}
