// 상세 모달 · 공유(등록) 모달 — 열기/닫기 및 공개범위 토글. import { $, $$, toast } from "./util.js"; import { applyReactions } from "./reactions.js"; // 저장소 URL 입력란의 빨간 안내(README 가져오기 전제)를 지운다. function clearRepoErr(m) { const err = $("#repoErr", m); const repoInput = $('input[name="repo_url"]', m); if (err) err.hidden = true; if (repoInput) repoInput.classList.remove("err"); } // 등록 모달의 "저장소 README 가져오기" — repo_url 의 README 를 받아 설명란을 채운다. async function importReadme() { const m = $("#shareModal"); if (!m) return; const btn = $("#readmeImport", m); const repoInput = $('input[name="repo_url"]', m); const err = $("#repoErr", m); const ta = $('textarea[name="description"]', m); const repo = ((repoInput && repoInput.value) || "").trim(); // URL 미입력 → 입력란 옆에 빨간 안내를 띄우고 포커스만 준다(토스트 대신 인라인). if (!repo) { if (err) err.hidden = false; if (repoInput) { repoInput.classList.add("err"); repoInput.focus(); } return; } clearRepoErr(m); if (ta && ta.value.trim() && !window.confirm("현재 설명 내용을 README 로 덮어쓸까요?")) return; const label = btn ? btn.innerHTML : ""; if (btn) { btn.disabled = true; btn.textContent = "가져오는 중…"; } try { const res = await fetch("/api/readme?repo_url=" + encodeURIComponent(repo), { headers: { "X-Requested-With": "fetch" } }); const data = res.ok ? await res.json() : null; if (data && data.content && ta) { ta.value = data.content; toast("README 를 가져왔어요"); ta.focus(); } else toast("README 를 찾지 못했어요 · URL 을 확인해 주세요"); } catch (e) { toast("가져오기에 실패했어요"); } finally { if (btn) { btn.disabled = false; btn.innerHTML = label; } } } export function openDetail(id) { const tpl = $('#detailStore template[data-detail-id="' + id + '"]'); const overlay = $("#detailModal"); if (!tpl || !overlay) { window.location.href = "/projects/" + id; return; } const mount = $(".modal", overlay) || overlay; mount.innerHTML = tpl.innerHTML; // 템플릿은 로드 시점 스냅샷이라 그동안 바뀐 반응 수가 반영돼 있지 않다. // 마운트 직후 현재 store 값으로 덮어써 카드와 항상 같은 값을 보이게 한다. applyReactions(mount); overlay.hidden = false; document.body.style.overflow = "hidden"; // 주의: 열기 감지 셀렉터가 [data-open-id] 라서 같은 이름을 쓰면 모달 내부 클릭(닫기/삭제)이 // 다시 "열기"로 잡힌다. 마커는 반드시 다른 속성명(data-open-pid)으로 둔다. overlay.setAttribute("data-open-pid", id); const ta = $(".comment-input", overlay); if (ta) ta.focus(); } function closeDetail() { const overlay = $("#detailModal"); if (!overlay || overlay.hidden) return; overlay.hidden = true; overlay.removeAttribute("data-open-pid"); document.body.style.overflow = ""; } // 폼 모달의 공개범위 토글을 지정 값으로 맞춘다(등록/수정 공용). function setVis(m, isPublic) { const input = $('input[name="is_public"]', m); if (input) input.value = isPublic ? "1" : "0"; $$("[data-vis]", m).forEach((b) => { const pub = b.getAttribute("data-vis") === "public"; b.classList.toggle("on-public", pub && isPublic); b.classList.toggle("on-private", !pub && !isPublic); }); const hint = $(".share-vishint", m); if (hint) hint.textContent = isPublic ? "피드에 공개되어 동료가 볼 수 있어요" : "나만 볼 수 있어요 · 언제든 공유로 전환 가능"; } function fieldSet(m, name, value) { const el = $('[name="' + name + '"]', m); if (el) el.value = value == null ? "" : value; } // 등록 모드로 초기화해서 연다. function openShare() { const m = $("#shareModal"); if (!m) return; const form = $("form", m); if (form) form.setAttribute("action", "/projects"); const h = $("#shareTitle", m); if (h) h.textContent = "새 프로젝트 등록"; const sub = $("#shareSubmit", m); if (sub) sub.textContent = "등록"; ["title", "description", "tags", "repo_url", "app_url", "lang", "return_to"].forEach((n) => fieldSet(m, n, "")); clearRepoErr(m); setVis(m, true); m.hidden = false; document.body.style.overflow = "hidden"; const t = $('input[name="title"]', m); if (t) t.focus(); } // 수정 모드: 상세의 '수정' 버튼 data-* 로 폼을 채우고 연다. function openEdit(btn) { const m = $("#shareModal"); if (!m || !btn) return; const form = $("form", m); if (form) form.setAttribute("action", "/projects/" + btn.getAttribute("data-id") + "/edit"); const h = $("#shareTitle", m); if (h) h.textContent = "프로젝트 수정"; const sub = $("#shareSubmit", m); if (sub) sub.textContent = "저장"; fieldSet(m, "title", btn.getAttribute("data-title")); fieldSet(m, "description", btn.getAttribute("data-desc")); fieldSet(m, "tags", btn.getAttribute("data-tags")); fieldSet(m, "repo_url", btn.getAttribute("data-repo")); fieldSet(m, "app_url", btn.getAttribute("data-app")); fieldSet(m, "lang", btn.getAttribute("data-lang")); fieldSet(m, "return_to", btn.getAttribute("data-return")); setVis(m, btn.getAttribute("data-public") === "1"); clearRepoErr(m); m.hidden = false; document.body.style.overflow = "hidden"; const t = $('input[name="title"]', m); if (t) { t.focus(); t.select(); } } function closeShare() { const m = $("#shareModal"); if (!m || m.hidden) return; m.hidden = true; document.body.style.overflow = ""; } export function initModals() { document.addEventListener("click", (e) => { // 상세 모달 열기 (카드 클릭 — 반응 버튼/링크/data-stop 은 제외) const opener = e.target.closest("[data-open-id]"); if (opener && !e.target.closest("[data-react]") && !e.target.closest("a") && !e.target.closest("[data-stop]")) { e.preventDefault(); openDetail(opener.getAttribute("data-open-id")); return; } if (e.target.closest("[data-close-detail]")) { e.preventDefault(); closeDetail(); return; } if (e.target.closest("[data-open-share]")) { e.preventDefault(); openShare(); return; } const editBtn = e.target.closest("[data-open-edit]"); if (editBtn) { e.preventDefault(); openEdit(editBtn); return; } if (e.target.closest("[data-close-share]")) { e.preventDefault(); closeShare(); return; } if (e.target.closest("#readmeImport")) { e.preventDefault(); importReadme(); return; } const detail = $("#detailModal"); if (detail && !detail.hidden && e.target === detail) { closeDetail(); return; } const share = $("#shareModal"); if (share && !share.hidden && e.target === share) { closeShare(); return; } // 공유 모달 공개/비공개 토글 const vo = e.target.closest("[data-vis]"); if (vo && share) { const val = vo.getAttribute("data-vis"); const input = $('input[name="is_public"]', share); if (input) input.value = val === "public" ? "1" : "0"; $$("[data-vis]", share).forEach((b) => { const pub = b.getAttribute("data-vis") === "public"; b.classList.toggle("on-public", pub && val === "public"); b.classList.toggle("on-private", !pub && val === "private"); }); const hint = $(".share-vishint", share); if (hint) hint.textContent = val === "public" ? "피드에 공개되어 동료가 볼 수 있어요" : "나만 볼 수 있어요 · 언제든 공유로 전환 가능"; } }); // 저장소 URL 을 입력하기 시작하면 빨간 안내를 즉시 지운다. document.addEventListener("input", (e) => { if (e.target && e.target.name === "repo_url") { const m = $("#shareModal"); if (m) clearRepoErr(m); } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeDetail(); closeShare(); } }); }