// editor.jsx — DOC-03 문서 에디터 const { Icon, Pill, Tag, Avatar, Button } = window.RAlly; const COL_LABEL = { todo: '시작 전', doing: '작성 중', review: '검토 중', done: '완주' }; const CAT_LABEL = { sop: 'SOP', risk: '위험관리', tech: '기술문서', capa: 'CAPA' }; const Editor = ({ go }) => { const [tab, setTab] = React.useState('comments'); const [aiAccepted, setAiAccepted] = React.useState(false); const [showShareToast, setShareToast] = React.useState(false); const docId = (typeof window !== 'undefined' && window.__RALLY_DOC) || null; const [doc, setDoc] = React.useState(null); const [comments, setComments] = React.useState([]); const [draft, setDraft] = React.useState(''); const bodyRef = React.useRef(null); const bodyInit = React.useRef(false); const [bodyHtml, setBodyHtml] = React.useState(''); const [saved, setSaved] = React.useState(true); React.useEffect(() => { if (bodyRef.current && bodyHtml && !bodyInit.current) { bodyRef.current.innerHTML = bodyHtml; bodyInit.current = true; } }, [bodyHtml]); const me = (window.RAlly.getUser && window.RAlly.getUser()) || null; const saveBody = () => { if (!docId || !bodyRef.current) return; // 권한 게이트는 서버가 세션에서 판정한다(클라이언트는 역할을 보내지 않는다). const headers = { 'Content-Type': 'application/json' }; fetch('/api/doc/' + docId + '/body', { method: 'POST', headers, body: JSON.stringify({ body: bodyRef.current.innerHTML }), }).then((r) => { if (!r.ok) { r.json().then((e) => alert(e.error || '저장 권한이 없습니다.')).catch(() => alert('저장 실패')); return; } setSaved(true); }).catch(() => {}); }; const loadComments = React.useCallback(() => { if (!docId) return; fetch('/api/doc/' + docId + '/comments').then((r) => r.json()).then((d) => setComments(d.comments || [])).catch(() => {}); }, [docId]); React.useEffect(() => { if (docId) { fetch('/api/review/doc?id=' + docId).then((r) => r.json()).then(setDoc).catch(() => {}); fetch('/api/doc/' + docId + '/body').then((r) => r.json()).then((d) => { if (d.ok) setBodyHtml(d.body); }).catch(() => {}); loadComments(); } }, [docId, loadComments]); const postComment = () => { if (!draft.trim() || !docId) return; const u = me || { name: '사용자', initials: '나' }; fetch('/api/doc/' + docId + '/comment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author: u.name, initials: u.initials, text: draft.trim() }), }).then((r) => r.json()).then((d) => { setComments(d.comments || []); setDraft(''); }).catch(() => {}); }; const requestReview = () => { if (!docId) return; // actor/actor_role은 보내지 않는다 — 서버가 세션에서 도출한다(역할 위조 방지). fetch('/api/kanban/move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ doc_id: docId, col: 'review' }), }).then((r) => r.json()).then((d) => { if (d.ok) go('kanban'); else alert(d.error); }).catch(() => {}); }; const [regen, setRegen] = React.useState(null); // null | busy | done | err const regenerate = () => { if (!docId) return; setRegen('busy'); fetch('/api/doc/' + docId + '/regenerate', { method: 'POST' }) .then((r) => r.json()).then((d) => { if (d.ok && d.body) { if (bodyRef.current) bodyRef.current.innerHTML = d.body; bodyInit.current = true; setBodyHtml(d.body); setSaved(true); setRegen('done'); } else { setRegen('err'); alert(d.error || '재생성 빌더가 없는 문서종입니다.'); } }).catch(() => setRegen('err')); }; React.useEffect(() => { if (showShareToast) { const t = setTimeout(() => setShareToast(false), 2400); return () => clearTimeout(t); } }, [showShareToast]); const title = doc ? doc.name : 'R-018 · 인터페이스 오작동 위험 평가 및 통제 방안'; const cat = doc ? (CAT_LABEL[doc.category] || doc.category) : '위험관리'; const colLabel = doc ? (COL_LABEL[doc.kanban_col] || '검토 중') : '검토 중'; // 개발자(AUTHOR) 권한 게이트: 열린 문서가 개발 문서(DEV_DOCS)가 아니면 편집 차단. // doc_type은 review/doc 응답 우선, 없으면 칸반 클릭 시 설정한 __RALLY_DOC_TYPE 폴백. const docType = (doc && doc.doc_type) || (typeof window !== 'undefined' && window.__RALLY_DOC_TYPE) || null; const blocked = !!(me && me.role === 'AUTHOR' && docType && !(window.RAlly.DEV_DOCS || []).includes(docType)); return (