// 모든 페이지 공통: API 호출, 오류 자동 보고, 작은 도우미. export const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); export const STATUS_LABEL = { draft: "검수 전", review: "검토 중", confirmed: "확정", rejected: "반려" }; export const FEEDBACK_STATUS_LABEL = { new: "접수", in_progress: "처리 중", resolved: "반영 완료", rejected: "반영 안 함", duplicate: "중복" }; export const REL_TYPE_LABEL = { part_of: "구성 요소", precedes: "다음 단계", informs: "자료로 활용", contrasts_with: "구분할 개념", related_to: "관련 개념", }; export const METHOD_LABEL = { wireframe: "시안 예시", curated: "비교 개념 초안", toc: "목차 구조", definition: "정의 문장", name: "이름 포함", manual: "직접 입력", auto: "자동 추출", draft: "AI 초안(교재 문장 기반)" }; export async function api(path, opts = {}) { const init = { credentials: "same-origin", ...opts, headers: { ...(opts.headers || {}) } }; if (opts.body && typeof opts.body !== "string") { init.body = JSON.stringify(opts.body); init.headers["Content-Type"] = "application/json"; } const res = await fetch(path, init); let data = null; const ct = res.headers.get("content-type") || ""; if (ct.includes("json")) data = await res.json(); else data = await res.text(); if (!res.ok) { const msg = (data && data.detail) ? (Array.isArray(data.detail) ? data.detail.map((d) => d.msg).join(", ") : data.detail) : `요청 실패 (${res.status})`; const err = new Error(msg); err.status = res.status; throw err; } return data; } let toastTimer; export function toast(msg, isError = false) { let el = document.querySelector(".toast"); if (!el) { el = document.createElement("div"); el.className = "toast"; el.setAttribute("role", "status"); document.body.appendChild(el); } el.textContent = msg; el.classList.toggle("error", isError); el.hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => (el.hidden = true), 3500); } // 화면 오류를 서버로 보내 관리자 피드백 목록(자동 수집 오류)에 쌓는다. const sent = new Set(); function report(message, stack, source) { const key = message + "|" + (stack || "").split("\n")[0]; if (sent.has(key) || sent.size > 20) return; sent.add(key); try { fetch("/api/client-errors", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: String(message).slice(0, 2000), stack: (stack || "").slice(0, 8000), url: location.href, source }), keepalive: true, }).catch(() => {}); } catch (_) { /* 보고 실패는 무시 */ } } window.addEventListener("error", (e) => report(e.message, e.error && e.error.stack, `${e.filename}:${e.lineno}:${e.colno}`)); window.addEventListener("unhandledrejection", (e) => { const r = e.reason || {}; if (r.status && r.status < 500) return; // 4xx는 사용자 입력 문제 report(r.message || String(r), r.stack, "promise"); }); export function fmtDate(s) { if (!s) return ""; return s.replace("T", " ").slice(0, 16); } export function debounce(fn, ms = 150) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; } export function qs(name) { return new URLSearchParams(location.search).get(name); }