- 상세(모달/단독) 소유자 영역에 '수정' 버튼 → 등록 모달을 현재 값으로 채워 재사용 - POST /projects/:id/edit 라우트 + db.updateProject(소유자 조건으로 타인 수정 차단) - 등록/수정 공용 모달을 _project_form_modal.ejs 로 추출(dashboard·project 에서 include) - pencil 아이콘 추가, modals.js 에 openEdit/openShare 초기화 로직 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
143 lines
7.0 KiB
JavaScript
143 lines
7.0 KiB
JavaScript
// 상세 모달 · 공유(등록) 모달 — 열기/닫기 및 공개범위 토글.
|
|
import { $, $$, toast } from "./util.js";
|
|
import { applyReactions } from "./reactions.js";
|
|
|
|
// 등록 모달의 "저장소 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 ta = $('textarea[name="description"]', m);
|
|
const repo = ((repoInput && repoInput.value) || "").trim();
|
|
if (!repo) { toast("먼저 저장소 URL 을 입력하세요"); if (repoInput) repoInput.focus(); return; }
|
|
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, ""));
|
|
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");
|
|
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" ? "피드에 공개되어 동료가 볼 수 있어요" : "나만 볼 수 있어요 · 언제든 공유로 전환 가능";
|
|
}
|
|
});
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeDetail(); closeShare(); } });
|
|
}
|