// ์•Œ๋ฆผ(๐Ÿ””) ์ข… โ€” ๋“œ๋กญ๋‹ค์šด ์—ด๊ธฐ/๋‹ซ๊ธฐ ยท ๋ชฉ๋ก fetch ยท ์•ˆ ์ฝ์€ ๋ฐฐ์ง€ ยท 60์ดˆ ํด๋ง. // ์„œ๋ฒ„ API: GET /api/notifications โ†’ { unread, items }, POST /api/notifications/read-all. import { $, toast } from "./util.js"; const POLL_MS = 60000; // ๋ฐฐ์ง€ ํด๋ง ์ฃผ๊ธฐ(60์ดˆ) // ์ข…๋ฅ˜๋ณ„ ์•„์ด์ฝ˜(๊ฐ€๋ฒผ์šด ์ด๋ชจ์ง€) ยท ์•Œ๋ฆผ ๋ฌธ๊ตฌ ยท ์ด๋™ ๋งํฌ. const ICONS = { heart: "โค๏ธ", star: "โญ", comment: "๐Ÿ’ฌ", announcement: "๐Ÿ“ข" }; function itemText(n) { const who = n.actor_name || "๋ˆ„๊ตฐ๊ฐ€"; const proj = n.project_title || "ํ”„๋กœ์ ํŠธ"; if (n.kind === "heart") return who + '๋‹˜์ด ยซ' + proj + 'ยป์— ์ข‹์•„์š”๋ฅผ ๋‚จ๊ฒผ์–ด์š”'; if (n.kind === "star") return who + '๋‹˜์ด ยซ' + proj + 'ยป์„(๋ฅผ) ์ฆ๊ฒจ์ฐพ๊ธฐํ–ˆ์–ด์š”'; if (n.kind === "comment") return who + '๋‹˜์ด ยซ' + proj + 'ยป์— ๋Œ“๊ธ€์„ ๋‚จ๊ฒผ์–ด์š”'; if (n.kind === "announcement") return "์ƒˆ ๊ณต์ง€: " + (n.announcement_title || ""); return "์ƒˆ ์•Œ๋ฆผ"; } function itemHref(n) { if (n.kind === "announcement") return "/announcements"; if (n.project_id) return "/projects/" + n.project_id; return "#"; } // ์ƒ๋Œ€ ์‹œ๊ฐ„(๋ฐฉ๊ธˆ/N๋ถ„ ์ „/N์‹œ๊ฐ„ ์ „/N์ผ ์ „/๋‚ ์งœ). function relTime(iso) { const t = new Date(iso).getTime(); if (!t) return ""; const s = Math.max(0, (Date.now() - t) / 1000); if (s < 60) return "๋ฐฉ๊ธˆ"; if (s < 3600) return Math.floor(s / 60) + "๋ถ„ ์ „"; if (s < 86400) return Math.floor(s / 3600) + "์‹œ๊ฐ„ ์ „"; if (s < 7 * 86400) return Math.floor(s / 86400) + "์ผ ์ „"; const d = new Date(t); return d.getFullYear() + "." + String(d.getMonth() + 1).padStart(2, "0") + "." + String(d.getDate()).padStart(2, "0"); } // ๋ชฉ๋ก ๋ Œ๋” โ€” textContent ๋กœ๋งŒ ์ฑ„์›Œ XSS ๋ฐฉ์ง€. function renderList(listEl, items) { listEl.textContent = ""; if (!items || !items.length) { const empty = document.createElement("div"); empty.className = "notif-empty"; empty.textContent = "์•„์ง ์•Œ๋ฆผ์ด ์—†์–ด์š”"; listEl.appendChild(empty); return; } items.forEach((n) => { const a = document.createElement("a"); a.className = "notif-item" + (n.is_read ? "" : " unread"); a.href = itemHref(n); const ico = document.createElement("span"); ico.className = "notif-item-ico"; ico.textContent = ICONS[n.kind] || "๐Ÿ””"; const body = document.createElement("span"); body.className = "notif-item-body"; const text = document.createElement("span"); text.className = "notif-item-text"; text.textContent = itemText(n); const time = document.createElement("span"); time.className = "notif-item-time"; time.textContent = relTime(n.created_at); body.appendChild(text); body.appendChild(time); a.appendChild(ico); a.appendChild(body); listEl.appendChild(a); }); } function setBadge(badgeEl, n) { const count = +n || 0; if (count > 0) { badgeEl.textContent = count > 99 ? "99+" : String(count); badgeEl.hidden = false; } else { badgeEl.hidden = true; } } function apiGet() { return fetch("/api/notifications", { headers: { "X-Requested-With": "fetch", "Accept": "application/json" } }) .then((r) => (r.ok ? r.json() : Promise.reject(r.status))); } function apiReadAll() { return fetch("/api/notifications/read-all", { method: "POST", headers: { "X-Requested-With": "fetch", "Accept": "application/json" }, }).then((r) => (r.ok ? r.json() : Promise.reject(r.status))); } export function initNotifications() { const btn = $("#notifBtn"), pop = $("#notifPop"), badge = $("#notifBadge"), list = $("#notifList"), readAll = $("#notifReadAll"); if (!btn || !pop || !badge || !list) return; // ๋น„๋กœ๊ทธ์ธ ๋“ฑ ์ข…์ด ์—†์œผ๋ฉด ๋ฌด๋™์ž‘ // ๋“œ๋กญ๋‹ค์šด ์—ด ๋•Œ: ๋ชฉ๋ก์„ ๋ฐ›์•„ ๋ Œ๋”ํ•œ ๋’ค ๋ชจ๋‘ ์ฝ์Œ ์ฒ˜๋ฆฌ โ†’ ๋ฐฐ์ง€ ์ดˆ๊ธฐํ™”. function openPanel() { apiGet() .then((data) => { renderList(list, data.items); setBadge(badge, data.unread); if (data.unread > 0) apiReadAll().then(() => setBadge(badge, 0)).catch(() => {}); }) .catch(() => { list.textContent = ""; const err = document.createElement("div"); err.className = "notif-empty"; err.textContent = "์•Œ๋ฆผ์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์–ด์š”"; list.appendChild(err); }); } btn.addEventListener("click", (e) => { e.stopPropagation(); const willOpen = pop.hidden; pop.hidden = !willOpen; btn.setAttribute("aria-expanded", willOpen ? "true" : "false"); if (willOpen) openPanel(); }); // ๋ฐ”๊นฅ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ(์‚ฌ์šฉ์ž ๋ฉ”๋‰ด์™€ ๋™์ผ ํŒจํ„ด). document.addEventListener("click", (e) => { if (!pop.hidden && !pop.contains(e.target) && !btn.contains(e.target)) { pop.hidden = true; btn.setAttribute("aria-expanded", "false"); } }); if (readAll) { readAll.addEventListener("click", (e) => { e.stopPropagation(); apiReadAll() .then(() => { setBadge(badge, 0); list.querySelectorAll(".notif-item.unread").forEach((el) => el.classList.remove("unread")); }) .catch(() => toast("์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.")); }); } // ์ตœ์ดˆ 1ํšŒ + ์ฃผ๊ธฐ์  ํด๋ง: ๋ฐฐ์ง€ ์ˆซ์ž๋งŒ ๊ฐฑ์‹ (๋“œ๋กญ๋‹ค์šด์ด ๋‹ซํ˜€ ์žˆ์„ ๋•Œ). function refreshBadge() { apiGet().then((data) => { if (pop.hidden) setBadge(badge, data.unread); }).catch(() => {}); } refreshBadge(); setInterval(refreshBadge, POLL_MS); }