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>
This commit is contained in:
2026-07-03 16:55:09 +09:00
parent 459f37608e
commit 7fe3a8fead
8 changed files with 168 additions and 62 deletions

View File

@@ -50,11 +50,53 @@ function closeDetail() {
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 = "";
@@ -71,6 +113,8 @@ export function initModals() {
}
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; }