/* 검토·피드백 문서 탭 3개: (기획안|디자인) · 아티클 · 기타
   - main/etc: 파일명 클릭 → 피그마식 주석 화면(변환 PDF or 이미지). 변환 실패 파일은 다운로드만.
   - article : 파일명 클릭 → 즉시 다운로드. LGE 포함 전체 업로드 가능.
   - main 탭에 미리보기 가능한 파일이 1개뿐이면 목록 생략하고 바로 주석 화면. */
/* 문서 뷰어 공용 — 작성자 역할별 대표색(LGE 빨강 / CNX·ADMIN 파랑) + 파일 크기 표기(최소 1KB, 0도 빈칸 안 됨). */
const annoColor = (role) => (role === 'LGE' ? ROLE_BADGE.LGE : ROLE_BADGE.CNX);
const fmtDocSize = (b) => (b > 1048576 ? (b / 1048576).toFixed(1) + 'MB' : Math.max(1, Math.round(b / 1024)) + 'KB');

/* 아티클 게시글 1개 — 접힘 행 + 펼침(원본 다운로드 · 댓글 스레드 · 작성창). 단일 열림은 부모가 제어. */
function ArticlePost({ doc, user, open, onToggle, onChanged }) {
  const isStaff = user.role === 'CNX' || user.role === 'ADMIN';
  const [comments, setComments] = useSE(null);
  const [body, setBody] = useSE('');
  const [files, setFiles] = useSE([]);      // 첨부 예정 File 객체 배열
  const [busy, setBusy] = useSE(false);
  const reload = () => window.api.getDocComments(doc.id).then(setComments).catch(() => setComments([]));
  useEE(() => { if (open) reload(); }, [open, doc.id]);
  const pickFiles = (e) => { const picked = Array.from(e.target.files || []); e.target.value = ''; setFiles((prev) => [...prev, ...picked]); };
  const rmFile = (i) => setFiles((prev) => prev.filter((_, k) => k !== i));
  const canSend = (body.trim() || files.length > 0) && !busy;
  const submit = async () => {
    if (!canSend) return;
    setBusy(true);
    // POST 응답이 곧 새 댓글(shapeComment 형태)이라 목록 재조회 없이 로컬에 붙인다. onChanged는 접힌 글 뱃지(commentCount) 갱신용.
    try { const c = await window.api.addDocComment(doc.id, body.trim(), files); setComments((prev) => [...(prev || []), c]); setBody(''); setFiles([]); onChanged && onChanged(); }
    catch (err) { alert('등록 실패: ' + (err.message || err)); }
    finally { setBusy(false); }
  };
  const delComment = async (c) => {
    if (!confirm('이 댓글을 삭제할까요?')) return;
    try { await window.api.deleteDocComment(c.id); setComments((prev) => (prev || []).filter((x) => x.id !== c.id)); onChanged && onChanged(); }
    catch (err) { alert('삭제 실패: ' + (err.message || err)); }
  };
  // 글(아티클 파일) 삭제 — 업로더 본인 or 스태프. onChanged로 부모 목록 재조회 → 이 글이 목록에서 사라짐.
  const delPost = async () => {
    if (!confirm(`'${doc.originalName}' 글을 삭제할까요? (댓글·첨부도 함께 접근 불가가 됩니다)`)) return;
    try { await window.api.deleteActionDoc(doc.id); onChanged && onChanged(); }
    catch (err) { alert('삭제 실패: ' + (err.message || err)); }
  };
  const canDel = (author) => (author && author.id === user.id) || isStaff;
  const count = open && comments ? comments.length : (doc.commentCount || 0);
  return (
    <div className="card fb-list" style={{ overflow: 'hidden' }}>
      <div className="fb-row" style={{ cursor: 'pointer' }} onClick={onToggle}>
        <span className="fb-task" title={doc.originalName} style={{ fontWeight: 600 }}>{doc.originalName}</span>
        <span className="fb-meta" style={{ fontSize: 11 }}>{doc.uploader.name} · {String(doc.createdAt).slice(0, 10)} · {fmtDocSize(doc.sizeBytes)}</span>
        {count > 0 && <span className="fb-count">댓글 {count}</span>}
        <span className="doc-anno-chev">{open ? '▲' : '▼'}</span>
      </div>
      {open && (
        <div style={{ padding: '10px 12px', borderTop: '1px solid var(--line)' }}>
          <div className="flex" style={{ marginBottom: 12, gap: 6 }}>
            <button className="pill" onClick={() => window.api.downloadActionDoc(doc.id, doc.originalName)}>⤓ 원본 다운로드</button>
            {canDel(doc.uploader) && <button className="pill" onClick={delPost} title="이 글(파일) 삭제" style={{ marginLeft: 'auto', color: 'var(--ink-4)' }}>글 삭제</button>}
          </div>
          {comments === null ? <div className="muted" style={{ fontSize: 12.5 }}>불러오는 중…</div>
            : comments.length === 0 ? <div className="muted" style={{ fontSize: 12.5, padding: '2px 0 10px' }}>아직 댓글이 없습니다.</div>
            : <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 }}>
                {comments.map((c) => {
                  const rc = annoColor(c.author.role);
                  return (
                    <div key={c.id} className="card" style={{ padding: '9px 11px' }}>
                      <div className="flex between center" style={{ marginBottom: 3 }}>
                        <span style={{ fontSize: 12.5 }}>
                          <b>{c.author.name}</b>
                          <span className="pill" style={{ marginLeft: 5, fontSize: 9.5, background: rc.bg, color: rc.c }}>{c.author.role}</span>
                          <span className="muted" style={{ marginLeft: 6, fontSize: 11 }}>{String(c.createdAt).slice(0, 10)}</span>
                        </span>
                        {canDel(c.author) && <button onClick={() => delComment(c)} title="삭제" style={{ border: 'none', background: 'transparent', color: 'var(--ink-4)', cursor: 'pointer', fontSize: 13 }}>✕</button>}
                      </div>
                      {c.body && <div style={{ fontSize: 13, lineHeight: 1.55, whiteSpace: 'pre-wrap', color: 'var(--ink-2)' }}>{c.body}</div>}
                      {c.files.length > 0 && (
                        <div className="flex" style={{ gap: 6, flexWrap: 'wrap', marginTop: c.body ? 6 : 0 }}>
                          {c.files.map((f) => (
                            <button key={f.id} className="pill" onClick={() => window.api.downloadCommentFile(f.id, f.originalName)} title="다운로드"
                              style={{ maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>📎 {f.originalName} · {fmtDocSize(f.sizeBytes)}</button>
                          ))}
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            <textarea value={body} onChange={(e) => setBody(e.target.value)} rows={2} placeholder="댓글을 남겨보세요…"
              style={{ fontSize: 13, padding: '8px 10px', borderRadius: 8, border: '1px solid var(--line)', resize: 'vertical', fontFamily: 'inherit' }} />
            {files.length > 0 && (
              <div className="flex" style={{ gap: 6, flexWrap: 'wrap' }}>
                {files.map((f, i) => (
                  <span key={i} className="pill" style={{ display: 'inline-flex', gap: 6, alignItems: 'center' }}>📎 {f.name}
                    <button onClick={() => rmFile(i)} style={{ border: 'none', background: 'transparent', cursor: 'pointer' }}>✕</button></span>
                ))}
              </div>
            )}
            <div className="flex" style={{ gap: 6 }}>
              <label className="pill" style={{ cursor: 'pointer' }}>＋ 파일 첨부
                <input type="file" multiple onChange={pickFiles} style={{ display: 'none' }} /></label>
              <button onClick={submit} disabled={!canSend}
                style={{ marginLeft: 'auto', background: canSend ? '#16181d' : 'var(--line)', color: canSend ? '#fff' : 'var(--ink-3)', border: 'none', borderRadius: 8, padding: '0 16px', fontWeight: 700, fontSize: 13, cursor: canSend ? 'pointer' : 'default' }}>{busy ? '…' : '등록'}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// initialPage·initialAnnoId = 알림 딥링크(주석/답글) — 그 주석이 달린 PDF 페이지로 열고 해당 주석을 펼친다.
function DocReviewTabs({ action, user, initialTab, initialDocId, initialPage, initialAnnoId }) {
  const isStaff = user.role === 'CNX' || user.role === 'ADMIN';
  const mainLabel = (action.task || '').includes('디자인') ? '디자인' : '기획안';
  // 운영상 기획안/디자인 한 종류만 쓰기로 해서 '아티클'·'기타' 탭은 숨긴다(2026-08-07).
  // 서버 API·업로드 카테고리는 그대로라 배열에 되돌려 넣으면 즉시 복구된다.
  const TABS = [['main', mainLabel]];
  // 탭별 업로드 허용 확장자 — main(기획안/디자인)=PDF 전용, 아티클=제한 없음, 기타=pdf·ppt·excel·이미지. (서버 ALLOWED_MIME와 일치)
  const ACCEPT = { main: '.pdf', article: undefined, etc: '.pdf,.ppt,.pptx,.xls,.xlsx,.jpg,.jpeg,.png' };
  // 숨긴 탭으로는 못 들어가게 막는다 — 옛 알림 딥링크(docTab='article' 등)를 눌러도 탭 버튼은
  // 없는데 그 탭 목록이 뜨는 어긋난 상태가 되지 않도록. TABS를 되돌리면 딥링크도 같이 살아난다.
  const [tab, setTab] = useSE(TABS.some(([k]) => k === initialTab) ? initialTab : 'main');
  const [docs, setDocs] = useSE(null);      // 현재 탭 파일 목록
  const [open, setOpen] = useSE(null);      // 주석 화면으로 연 doc
  const [busy, setBusy] = useSE(false);
  const [openPostId, setOpenPostId] = useSE(null);   // 아티클 탭: 펼친 게시글 id(단일 열림)
  // 딥링크가 지금 '실제로 연' 문서 {docId, page, annoId}. 사용자가 손으로 목록/탭을 건드리면 즉시 비운다 —
  // 이게 없으면(문서 id 비교만 하면) 목록으로 나갔다 같은 문서를 다시 열 때마다 계속 그 페이지로 튄다.
  const [dlTarget, setDlTarget] = useSE(null);
  const canUpload = isStaff;   // 업로드는 CNX·담당자(스태프)만 — LGE에게는 버튼 자체를 숨긴다
  const autoOpened = useRef(false);   // 탭/항목 진입당 1회만 자동 열기 — 이후 업로드·삭제 reload가 목록을 다시 낚아채지 않게
  const reload = () => window.api.getActionDocs(action.id, tab).then(setDocs).catch(() => setDocs([]));
  useEE(() => { setDocs(null); setOpen(null); setDlTarget(null); autoOpened.current = false; reload(); }, [action.id, tab]);
  const dlDocDone = React.useRef(false);
  useEE(() => {
    if (!initialDocId || dlDocDone.current || !docs) return;
    const d = docs.find((x) => x.id === initialDocId);
    if (!d) { dlDocDone.current = true; return; }
    dlDocDone.current = true;
    autoOpened.current = true;   // 딥링크가 문서를 지정했으니 '최신 PDF 자동열기'를 억제(덮어쓰기 방지)
    if (tab === 'article') { setOpenPostId(d.id); }            // 아티클: 그 글 펼침
    else if (d.hasPdf || d.isImage) { setOpen(d); setDlTarget({ docId: d.id, page: initialPage, annoId: initialAnnoId }); }
  }, [docs]);
  // main 탭: 진입 시 1회, 가장 최근 PDF를 자동으로 주석 화면에 띄운다(docs는 최신순 정렬 → 첫 PDF = 최신).
  useEE(() => {
    if (tab !== 'main' || !docs || open || autoOpened.current) return;
    const latestPdf = docs.find((d) => d.hasPdf);
    if (latestPdf) { autoOpened.current = true; setOpen(latestPdf); }
  }, [docs]);
  const onUpload = async (e) => {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    setBusy(true);
    try { await window.api.uploadActionDocFile(action.id, tab, f); reload(); }
    catch (err) { alert('업로드 실패: ' + (err.message || err)); }
    finally { setBusy(false); e.target.value = ''; }
  };
  const del = async (d) => {
    if (!confirm(`'${d.originalName}' 파일을 삭제할까요? (주석도 함께 숨겨집니다)`)) return;
    try { await window.api.deleteActionDoc(d.id); if (open && open.id === d.id) setOpen(null); reload(); }
    catch (err) { alert('삭제 실패: ' + (err.message || err)); }
  };
  const clickDoc = (d) => {
    setDlTarget(null);   // 손으로 연 문서 — 딥링크의 페이지·주석을 더는 적용하지 않는다(1페이지부터)
    if (d.hasPdf || d.isImage) setOpen(d);
    else window.api.downloadActionDoc(d.id, d.originalName);
  };
  // 항목 전체(기획안·아티클·기타) → zip. 파일이 하나도 없으면 서버가 404 → 사용자에게 안내.
  const downloadAll = async () => {
    try { await window.api.downloadActionDocsZip(action.id, action.task); }
    catch (e) { alert(e && e.message ? e.message : '다운로드할 파일이 없습니다.'); }
  };
  return (
    <div>
      <div className="flex between center" style={{ gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
        <h3 style={{ fontWeight: 750, fontSize: 18, letterSpacing: '-.01em', margin: 0 }}>{action.task}</h3>
        <span className="flex center" style={{ gap: 8 }}>
          <button className="pill" onClick={downloadAll} title="이 항목의 모든 파일을 zip으로 내려받기">⤓ 전체 다운로드</button>
          {canUpload && <label className="pill pill-fill" style={{ cursor: 'pointer' }}>{busy ? '업로드 중…' : '＋ 파일 업로드'}
            <input type="file" onChange={onUpload} style={{ display: 'none' }} accept={ACCEPT[tab]} /></label>}
        </span>
      </div>
      {/* 문서 탭 — 앱 표준 세그먼트 컨트롤(현황/피드백 L2와 동일). 활성색은 앱 accent(초록 아님) */}
      <div className="seg" style={{ marginBottom: 12 }}>
        {TABS.map(([k, l]) => <button key={k} className={tab === k ? 'on red' : ''} onClick={() => setTab(k)}>{l}</button>)}
      </div>
      {open ? (
        <div>
          <div className="doc-open-head">
            <div className="doc-open-file">
              <button className="pill" onClick={() => { setOpen(null); setDlTarget(null); }}>← 파일 목록</button>
              <b>{open.originalName}</b>
            </div>
            <button className="pill" onClick={() => window.api.downloadActionDoc(open.id, open.originalName)}>⤓ 원본 다운로드</button>
          </div>
          {/* 딥링크 페이지·주석은 그 딥링크가 방금 연 문서에만 적용 — 손으로 연 문서는 언제나 1페이지부터 */}
          <DocAnnotator key={open.id} doc={open} user={user}
            initialPage={dlTarget && dlTarget.docId === open.id ? dlTarget.page : undefined}
            initialAnnoId={dlTarget && dlTarget.docId === open.id ? dlTarget.annoId : undefined} />
        </div>
      ) : !docs ? <div className="muted" style={{ padding: 12 }}>불러오는 중…</div>
        : docs.length === 0 ? <div className="muted" style={{ padding: 24, textAlign: 'center' }}>등록된 파일이 없습니다.{canUpload ? ' 위 버튼으로 올려보세요.' : ''}</div>
        : tab === 'article' ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {docs.map((d) => (
            <ArticlePost key={d.id} doc={d} user={user}
              open={openPostId === d.id}
              onToggle={() => setOpenPostId((id) => (id === d.id ? null : d.id))}
              onChanged={reload} />
          ))}
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {docs.map((d) => (
            <div key={d.id} className="card flex between center" style={{ padding: '9px 12px' }}>
              <a onClick={() => clickDoc(d)} style={{ cursor: 'pointer', fontSize: 13, fontWeight: 600, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {d.originalName}
                {!d.hasPdf && !d.isImage && <span className="pill" style={{ marginLeft: 6, fontSize: 9.5 }}>미리보기 불가</span>}
                {d.annotationCount > 0 && <span className="fb-count" style={{ marginLeft: 6 }}>주석 {d.annotationCount}</span>}
              </a>
              <span className="flex center" style={{ gap: 8, flexShrink: 0 }}>
                <span className="muted" style={{ fontSize: 11 }}>{d.uploader.name} · {String(d.createdAt).slice(0, 10)} · {fmtDocSize(d.sizeBytes)}</span>
                {(isStaff || d.uploader.id === user.id) && <button onClick={() => del(d)} title="삭제" style={{ border: 'none', background: 'transparent', color: 'var(--ink-4)', cursor: 'pointer' }}>✕</button>}
              </span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------------- Doc annotator (검토·피드백 액션의 PDF/이미지 리뷰, doc 단위) ---------------- */
if (window.pdfjsLib) window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'app/vendor/pdf.worker.min.js';

function DocAnnotator({ doc, user, initialPage, initialAnnoId }) {
  const isStaff = user.role === 'CNX' || user.role === 'ADMIN';
  // 영역 주석 대표색은 모듈 상단 annoColor(LGE 빨강 / CNX·ADMIN 파랑) 재사용. 전역 ROLE_BADGE(뱃지용)와 별개 규칙.
  const [pdf, setPdf] = useSE(null);        // pdfjs document
  const [page, setPage] = useSE(1);
  const [nPages, setNPages] = useSE(0);
  const [imgUrl, setImgUrl] = useSE(null);  // 이미지 모드 blob URL
  const [renderTick, setRenderTick] = useSE(0);  // 폭 변화 시 재렌더 트리거(선명도 유지)
  const [zoom, setZoom] = useSE(1);              // 확대 배율(1=폭맞춤). 확대 시 고해상도 재렌더 + 가로 스크롤
  const canvasRef = useRef(null);
  const wrapRef = useRef(null);
  const scrollRef = useRef(null);                // 문서 스크롤 컨테이너(확대 시 가로 스크롤 기준 폭)
  const blobRef = useRef(null);
  const dragRef = useRef(null);   // 라이브 드래그 사각형(0~1) — 렌더 클로저 지연 회피
  // 문서 로드 → pdfjs (hasPdf) 또는 이미지(isImage)
  useEE(() => {
    if (!doc.hasPdf || !window.pdfjsLib) { setPdf(null); return; }
    let cancelled = false;
    setPage(1);                               // 교체(짧은 새 문서 포함) 시 항상 1페이지부터 → page 클램프
    window.api.fetchBlobUrl('/docs/' + doc.id + '/pdf').then((bu) => {
      if (cancelled) { URL.revokeObjectURL(bu); return null; }
      if (blobRef.current) URL.revokeObjectURL(blobRef.current);   // 이전 blob 해제(누수 방지)
      blobRef.current = bu;
      return window.pdfjsLib.getDocument(bu).promise;
    }).then((d) => { if (d && !cancelled) { setPdf(d); setNPages(d.numPages); } })
      .catch(() => { if (!cancelled) { setPdf(null); setNPages(0); alert('문서를 불러오지 못했습니다.'); } });
    return () => { cancelled = true; };
  }, [doc]);
  // 알림 딥링크: 주석이 달린 페이지로 점프. pdf 로드가 끝난 뒤(numPages 확정)에 클램프해서 적용한다.
  // 로드 이펙트와 분리해 둔 이유 — initialPage 를 그 이펙트의 deps 에 넣으면 blob 을 다시 받고,
  // 안 넣으면 '부모가 매번 remount 된다'는 app.jsx 의 key 규칙에 암묵적으로 기대게 된다. 여기선 둘 다 아니다.
  useEE(() => {
    if (!pdf || !initialPage) return;
    setPage(Math.min(Math.max(1, initialPage), pdf.numPages));
  }, [pdf, initialPage]);
  useEE(() => {
    if (!doc.isImage) { setImgUrl(null); return; }
    let cancelled = false;
    setPage(1); setNPages(1);
    window.api.fetchBlobUrl('/docs/' + doc.id + '/file').then((bu) => {
      if (cancelled) { URL.revokeObjectURL(bu); return; }
      if (blobRef.current) URL.revokeObjectURL(blobRef.current);
      blobRef.current = bu;
      setImgUrl(bu);
    }).catch(() => { if (!cancelled) alert('이미지를 불러오지 못했습니다.'); });
    return () => { cancelled = true; };
  }, [doc]);
  // 페이지 렌더 (PDF 모드만) — 표시 폭 × DPR × 오버샘플 만큼 고해상도로 그려 뷰어급 선명도.
  useEE(() => {
    if (!pdf || !canvasRef.current || !wrapRef.current || !scrollRef.current) return;
    let task = null; let cancelled = false;
    pdf.getPage(page).then((pg) => {
      if (cancelled) return;
      const cv = canvasRef.current; const wrap = wrapRef.current; const scroll = scrollRef.current;
      if (!cv || !wrap || !scroll) return;
      const base = pg.getViewport({ scale: 1 });               // PDF 원본 좌표계
      const metrics = window.LGDocRender.computePdfRenderMetrics({
        baseWidth: base.width,
        baseHeight: base.height,
        availableWidth: scroll.clientWidth || base.width,
        zoom,
        dpr: window.devicePixelRatio || 1,
      });
      const vp = pg.getViewport({ scale: metrics.viewportScale });
      wrap.style.width = metrics.cssWidth + 'px';              // 확대 시 컨테이너 넘어 → 가로 스크롤
      cv.width = metrics.canvasWidth; cv.height = metrics.canvasHeight;
      cv.style.width = metrics.cssWidth + 'px';
      cv.style.height = metrics.cssHeight + 'px';
      window.__lgaiLastPdfRender = { docId: doc.id, page, dpr: window.devicePixelRatio || 1, ...metrics };
      task = pg.render({ canvasContext: cv.getContext('2d'), viewport: vp, transform: metrics.transform });
      task.promise.catch(() => {});          // cancel 시 reject 무시
    }).catch(() => {});
    return () => { cancelled = true; if (task) task.cancel(); };
  }, [pdf, page, renderTick, zoom]);
  // 이미지 모드: 확대/폭 변화 시 wrap 표시 폭만 갱신(래스터라 재렌더 없음, CSS 스케일).
  useEE(() => {
    if (!doc.isImage || !wrapRef.current || !scrollRef.current) return;
    const avail = scrollRef.current.clientWidth || 1;
    wrapRef.current.style.width = Math.max(1, Math.round(avail * zoom)) + 'px';
  }, [imgUrl, zoom, renderTick]);
  // 컨테이너 폭 변화(창 크기·목록 접기) 감지 → 재렌더. 폭이 실제로 바뀔 때만(높이 변화 루프 방지).
  useEE(() => {
    const scroll = scrollRef.current;
    if (!scroll || typeof ResizeObserver === 'undefined') return;
    let lastW = scroll.clientWidth; let t = null;
    const ro = new ResizeObserver(() => {
      const w = scroll.clientWidth;
      if (w === lastW) return;
      lastW = w;
      clearTimeout(t); t = setTimeout(() => setRenderTick((n) => n + 1), 150);
    });
    ro.observe(scroll);
    return () => { clearTimeout(t); ro.disconnect(); };
  }, [pdf, doc.isImage]);
  // Task 4: 주석 상태 + 로드
  const [annos, setAnnos] = useSE([]);
  const [sel, setSel] = useSE(null);        // 선택=펼친 주석 id (PDF 박스 하이라이트 + 리스트에서 펼침). 아코디언식 단일 열림
  const [drag, setDrag] = useSE(null);      // {x0,y0,x1,y1} (0~1)
  const [draftBody, setDraftBody] = useSE('');
  const [reply, setReply] = useSE('');
  const reloadAnnos = () => window.api.getAnnotations(doc.id).then(setAnnos).catch(() => setAnnos([]));
  useEE(() => { reloadAnnos(); }, [doc.id]);
  // 페이지가 바뀌면 그리기·입력 상태를 리셋한다. 단 sel(펼친 주석)은 '현재 페이지에 속하지 않을 때만' 지운다 —
  // 무조건 null 로 지우면 아래 딥링크 이펙트가 편 주석을 이 이펙트가 덮어쓸 수 있어 선언 순서에 의존하게 된다.
  useEE(() => {
    dragRef.current = null; setDrag(null); setDraftBody(''); setReply('');
    setSel((s) => (s && annos.some((a) => a.id === s && a.page === page) ? s : null));
  }, [page]);
  const toggleAnno = (id) => { setSel((s) => (s === id ? null : id)); setReply(''); };
  const openAnno = (id) => { setSel(id); setReply(''); setTimeout(() => { const el = document.getElementById('anno-' + id); if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }, 0); };
  const pageAnnos = annos.filter((a) => a.page === page);
  // 알림 딥링크가 지목한 주석 펼치기(1회). 목표 페이지에 실제로 도달한 뒤에만 편다
  // (PDF 로드 → setPage 는 비동기라 첫 커밋엔 아직 1페이지).
  const dlAnnoDone = useRef(false);
  useEE(() => {
    if (!initialAnnoId || dlAnnoDone.current || !annos.length) return;
    const a = annos.find((x) => x.id === initialAnnoId);
    if (!a) { dlAnnoDone.current = true; return; }   // 삭제된 주석 — 페이지 이동까지는 이미 됨
    if (a.page !== page) return;                     // 아직 목표 페이지 전 → 다음 커밋에서 재시도
    dlAnnoDone.current = true;
    openAnno(a.id);
  }, [annos, page]);
  const canDel = (author) => (author && author.id === user.id) || isStaff;
  // Task 4: 드래그로 영역 그리기
  const rel = (e) => { const r = wrapRef.current.getBoundingClientRect(); const cl = (v) => Math.max(0, Math.min(1, v)); return { x: cl((e.clientX - r.left) / r.width), y: cl((e.clientY - r.top) / r.height) }; };
  const onDown = (e) => {
    if (e.target.dataset.box) return;
    e.preventDefault();
    const p = rel(e);
    dragRef.current = { x0: p.x, y0: p.y, x1: p.x, y1: p.y };
    setDrag({ ...dragRef.current }); setSel(null);
    const move = (ev) => { if (!dragRef.current) return; const q = rel(ev); dragRef.current = { ...dragRef.current, x1: q.x, y1: q.y }; setDrag({ ...dragRef.current }); };
    const up = () => {
      window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up);
      const d = dragRef.current; dragRef.current = null; if (!d) return;
      const x = Math.min(d.x0, d.x1), y = Math.min(d.y0, d.y1), w = Math.abs(d.x1 - d.x0), h = Math.abs(d.y1 - d.y0);
      if (w < 0.01 || h < 0.01) { setDrag(null); return; }
      setDrag({ x0: x, y0: y, x1: x + w, y1: y + h, pending: true });
    };
    window.addEventListener('mousemove', move); window.addEventListener('mouseup', up);
  };
  const saveDraft = async () => {
    if (!draftBody.trim()) return; const d = drag;
    try { await window.api.createAnnotation(doc.id, { page, x: Math.min(d.x0, d.x1), y: Math.min(d.y0, d.y1), w: Math.abs(d.x1 - d.x0), h: Math.abs(d.y1 - d.y0), body: draftBody.trim(), kind: 'box' }); setDrag(null); setDraftBody(''); reloadAnnos(); }
    catch (e) { alert('주석 저장 실패: ' + (e.message || e)); }
  };
  return (
    <div>
      {/* 컨트롤 바 — 좌: 페이지 이동, 우: 확대/축소 (크게, 세그먼트형) */}
      <div className="doc-ctrlbar">
        {doc.hasPdf ? (
          <div className="doc-ctrl">
            <button title="이전 페이지" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>◀</button>
            <span className="doc-ctrl-val">{page} / {nPages || '…'}</span>
            <button title="다음 페이지" disabled={page >= nPages} onClick={() => setPage((p) => p + 1)}>▶</button>
          </div>
        ) : <div />}
        {/* 주석 포함 PDF — 원본에 주석을 PDF 주석으로 심어 내려받는다(Acrobat 주석 패널에 뜸) */}
        <div className="doc-ctrl">
          <button title="주석이 메모로 심긴 PDF를 내려받습니다" disabled={!doc.hasPdf || !annos.length}
            onClick={() => window.api.downloadAnnotatedPdf(doc.id, doc.originalName.replace(/\.[^.]+$/, '') + '_주석.pdf')
              .catch((e) => alert('내려받기 실패: ' + (e.message || e)))}>
            주석 포함 PDF{annos.length ? ` (${annos.length})` : ''}
          </button>
        </div>
        {/* 확대/축소 — 작은 글씨는 확대하면 고해상도로 다시 그려 선명해집니다 */}
        <div className="doc-ctrl">
          <button title="축소" disabled={zoom <= 0.5} onClick={() => setZoom((z) => Math.max(0.5, Math.round((z - 0.25) * 100) / 100))}>－</button>
          <button className="doc-ctrl-val" title="폭 맞춤" onClick={() => setZoom(1)}>{Math.round(zoom * 100)}%</button>
          <button title="확대" disabled={zoom >= 4} onClick={() => setZoom((z) => Math.min(4, Math.round((z + 0.25) * 100) / 100))}>＋</button>
        </div>
      </div>

      {/* PDF 풀폭 */}
      <div ref={scrollRef} style={{ overflowX: 'auto', maxWidth: '100%' }}>
        <div ref={wrapRef} className="doc-canvas-wrap">
          {doc.isImage
            ? (imgUrl && <img src={imgUrl} style={{ display: 'block', width: '100%', height: 'auto' }} />)
            : <canvas ref={canvasRef} style={{ display: 'block', width: '100%', height: 'auto' }} />}
          <div onMouseDown={onDown}
            style={{ position: 'absolute', inset: 0, cursor: 'crosshair', userSelect: 'none' }}>
            {pageAnnos.map((a, i) => {
              const rc = annoColor(a.author.role);   // 작성자 역할별 대표색(LGE 빨강 / CNX·ADMIN 파랑)
              const hl = a.kind === 'highlight';                        // 형광펜=반투명 마커, 네모=반투명+테두리
              return (
              <div key={a.id} data-box="1" onClick={(e) => { e.stopPropagation(); openAnno(a.id); }}
                style={{ position: 'absolute', left: `${a.x * 100}%`, top: `${a.y * 100}%`, width: `${a.w * 100}%`, height: `${a.h * 100}%`,
                  border: hl ? (sel === a.id ? `2px solid ${rc.c}` : 'none') : `${sel === a.id ? 3 : 2}px solid ${rc.c}`,
                  background: rc.c + (hl ? '4d' : '20'), opacity: sel && sel !== a.id ? 0.45 : 1, cursor: 'pointer' }}>
                <span data-box="1" style={{ position: 'absolute', top: -9, left: -9, background: rc.c, color: '#fff', borderRadius: 10, fontSize: 10, padding: '0 5px' }}>{i + 1}</span>
              </div>
            );})}
            {drag && (() => { const uc = annoColor(user.role); return <div style={{ position: 'absolute', left: `${Math.min(drag.x0, drag.x1) * 100}%`, top: `${Math.min(drag.y0, drag.y1) * 100}%`, width: `${Math.abs(drag.x1 - drag.x0) * 100}%`, height: `${Math.abs(drag.y1 - drag.y0) * 100}%`, border: `2px dashed ${uc.c}`, background: uc.c + '20' }} />; })()}
          </div>
        </div>
      </div>

      {/* 주석 리스트업 — PDF 아래 풀폭 그리드 */}
      <div className="doc-annos">
        <div className="doc-annos-head">
          이 페이지 주석 <span className="fb-count">{pageAnnos.length}</span>
          <span className="muted" style={{ fontSize: 12, fontWeight: 400 }}>· PDF 위 영역을 드래그해 새 주석을 남기세요</span>
        </div>
        {drag && drag.pending && (
          <div className="card" style={{ padding: 12, boxShadow: '0 0 0 2px #16181d', marginBottom: 8 }}>
            <div style={{ fontSize: 13, fontWeight: 750, marginBottom: 8 }}>새 주석</div>
            <textarea value={draftBody} onChange={(e) => setDraftBody(e.target.value)} rows={3} placeholder="코멘트를 입력하세요…" style={{ width: '100%', fontSize: 13, padding: 9, borderRadius: 8, border: '1px solid var(--line)', fontFamily: 'inherit', resize: 'vertical' }} />
            <div className="flex" style={{ gap: 6, marginTop: 8 }}><button className="pill pill-fill" onClick={saveDraft} disabled={!draftBody.trim()}>등록</button><button className="pill" onClick={() => { setDrag(null); setDraftBody(''); }}>취소</button></div>
          </div>
        )}
        {pageAnnos.length === 0 && !(drag && drag.pending)
          ? <div className="doc-annos-empty">아직 주석이 없습니다. 위 문서에서 영역을 드래그해 첫 주석을 남겨보세요.</div>
          : (
          <div className="doc-annos-list">
            {pageAnnos.map((a, i) => {
              const rc = annoColor(a.author.role);
              const open = sel === a.id;   // 아코디언 — 선택된 주석만 펼침
              const replies = a.replies || [];
              return (
                <div key={a.id} id={'anno-' + a.id} className="doc-anno" style={{ boxShadow: sel === a.id ? `0 0 0 2px ${rc.c}` : undefined }}>
                  {/* 접힘 행 — 항상 한 줄(본문 …말줄임). 클릭=펼침/접힘 토글 */}
                  <div className="doc-anno-row" onClick={() => toggleAnno(a.id)}>
                    <span className="doc-anno-num" style={{ background: rc.c }}>{i + 1}</span>
                    <span className="doc-anno-who">{a.author.name}</span>
                    <span className="pill" style={{ fontSize: 9.5, background: rc.bg, color: rc.c }}>{a.author.role}</span>
                    {!open && <span className="doc-anno-prev">{a.body}</span>}
                    {!open && replies.length > 0 && <span className="fb-count">답글 {replies.length}</span>}
                    <span className="doc-anno-chev">{open ? '▲' : '▼'}</span>
                  </div>
                  {open && (
                    <div className="doc-anno-open" onClick={(e) => e.stopPropagation()}>
                      <div className="doc-anno-body">{a.body}</div>
                      {replies.map((r) => { const rrc = annoColor(r.author.role); return (
                        <div key={r.id} className="doc-anno-reply">
                          <b>{r.author.name}</b> <span className="pill" style={{ fontSize: 9, background: rrc.bg, color: rrc.c }}>{r.author.role}</span>
                          {canDel(r.author) && <button onClick={async (e) => { e.stopPropagation(); try { await window.api.deleteAnnotationReply(r.id); reloadAnnos(); } catch (err) { alert('삭제 실패: ' + (err.message || err)); } }} style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 11 }}>✕</button>}
                          <div style={{ whiteSpace: 'pre-wrap', marginTop: 2 }}>{r.body}</div>
                        </div>
                      ); })}
                      <div className="flex" style={{ gap: 6, marginTop: 8 }}>
                        <input value={reply} onChange={(e) => setReply(e.target.value)} placeholder="답글 남기기…" style={{ flex: 1, fontSize: 12.5, padding: '6px 9px', borderRadius: 8, border: '1px solid var(--line)' }} />
                        <button className="pill" disabled={!reply.trim()} onClick={async () => { await window.api.replyAnnotation(a.id, reply.trim()).catch((err) => alert(err.message || err)); setReply(''); reloadAnnos(); }}>등록</button>
                        {canDel(a.author) && <button className="pill" onClick={async () => { if (confirm('주석 삭제?')) { try { await window.api.deleteAnnotation(a.id); setSel(null); reloadAnnos(); } catch (err) { alert('삭제 실패: ' + (err.message || err)); } } }}>주석 삭제</button>}
                      </div>
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}
