diff --git a/public/js/modals.js b/public/js/modals.js
index 1d7d996..1589972 100644
--- a/public/js/modals.js
+++ b/public/js/modals.js
@@ -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; }
diff --git a/src/db.js b/src/db.js
index 4ece6e3..9c6260a 100644
--- a/src/db.js
+++ b/src/db.js
@@ -116,6 +116,17 @@ export async function addProject({ owner, title, description, repoUrl, appUrl, t
return rows[0].id;
}
+// 본인 프로젝트 수정. 적용 행 수 반환(0이면 권한 없음/없음). 소유자 조건으로 타인 수정 차단.
+export async function updateProject({ id, sabun, title, description, repoUrl, appUrl, tags, isPublic, lang }) {
+ const { rowCount } = await q(
+ `UPDATE projects
+ SET title=$3, description=$4, repo_url=$5, app_url=$6, tags=$7, is_public=$8, lang=$9, updated_at=now()
+ WHERE id=$1 AND owner_sabun=$2`,
+ [id, sabun, title, description || "", repoUrl || "", appUrl || "", tags || "", isPublic !== false, lang || ""]
+ );
+ return rowCount;
+}
+
// 본인 프로젝트 삭제. 삭제 행 수 반환(0이면 권한 없음/없음).
export async function deleteProject({ id, sabun }) {
const { rowCount } = await q(
diff --git a/src/helpers.js b/src/helpers.js
index b1df278..f97b85d 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -28,6 +28,7 @@ const OCTICONS = {
ext: '