Files
ai-dev-portal/public/js/modals.js
2620227 7fe3a8fead feat(projects): 등록한 프로젝트 수정 기능 추가
- 상세(모달/단독) 소유자 영역에 '수정' 버튼 → 등록 모달을 현재 값으로 채워 재사용
- 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>
2026-07-03 16:55:09 +09:00

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(); } });
}