Files
jsahn 4e943700fc feat: add common utilities and feedback handling scripts
- Introduced common.js for shared functions including API calls, error reporting, and UI helpers.
- Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status.
- Added layout.js for managing galaxy and section layouts in the visualization.
- Created login.js for user authentication with session management.
- Developed search.js for term searching based on user input and synonyms.
- Implemented signup.js for user registration with validation and immediate login.
2026-10-06 23:14:45 +09:00

87 lines
3.4 KiB
JavaScript

// 모든 페이지 공통: API 호출, 오류 자동 보고, 작은 도우미.
export const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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);
}