feat(project): 완료 토글, 설명 이미지 크기조절/삭제, 연결폼 가독성, 모달 고정크기 800x1100

- 업무 편집 모달에 "완료로 표시" 버튼 추가(완료 시 리스트뷰는 기존 취소선
  스타일 그대로 적용됨). 완료 상태에 따라 버튼 색/문구 토글.
- 설명란 이미지 클릭 시 선택되어 우하단 드래그로 크기 조절, × 버튼으로 삭제
  가능. 크기는 style이 아닌 width 속성으로 저장해 서버 sanitize 후에도 유지.
- 연결된 프로젝트 select 를 세로 배치+작은 폰트로 바꿔 글자 잘림 해결.
- 업무 편집 모달을 800x1100 고정 크기로 바꾸고 좌(메타/설명)·우(하위업무·
  첨부·연결·댓글) 칼럼이 각자 내부 스크롤하도록 재정비.

fix(shell): 사이드바 접힘 상태를 localStorage에서 첫 페인트 전에 반영해
페이지 이동 시 "펼쳐졌다 다시 접히는" 깜빡임 제거.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 21:02:37 +09:00
parent 8449b404b2
commit b3d6a35651
6 changed files with 169 additions and 26 deletions
+99 -1
View File
@@ -348,6 +348,8 @@
el("pj-t-dtime").disabled = !useTime;
el("pj-t-open-project").href = "/project/p/" + task.project_id + "?task=" + task.id;
el("pj-delete-task").hidden = false;
el("pj-toggle-complete").hidden = false;
setHomeCompleteBtn(!!task.completed_at);
el("pj-t-open-project").hidden = false;
el("pj-attach-block").hidden = false;
el("pj-comment-block").hidden = false;
@@ -377,6 +379,7 @@
el("pj-t-stime").value = ""; el("pj-t-stime").disabled = true;
el("pj-t-dtime").value = ""; el("pj-t-dtime").disabled = true;
el("pj-delete-task").hidden = true;
el("pj-toggle-complete").hidden = true;
el("pj-t-open-project").hidden = true;
el("pj-attach-block").hidden = true;
el("pj-comment-block").hidden = true;
@@ -385,6 +388,21 @@
openModal(homeTaskModal);
await Promise.all([fillAssignee(projectId, ""), fillStage(projectId, null)]);
}
function setHomeCompleteBtn(done) {
const btn = el("pj-toggle-complete");
btn.textContent = done ? "↩ 완료 취소" : "✓ 완료로 표시";
btn.classList.toggle("is-done", done);
btn.dataset.completed = done ? "1" : "";
}
el("pj-toggle-complete").addEventListener("click", async function () {
const id = el("pj-t-id").value;
if (!id) return;
const done = el("pj-toggle-complete").dataset.completed === "1";
try {
await api("PUT", "/project/api/tasks/" + id, { completed: !done });
location.reload();
} catch (e) { alert("처리 실패: " + e.message); }
});
document.querySelectorAll(".pj-card-addtask").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.preventDefault(); e.stopPropagation();
@@ -1584,6 +1602,8 @@
el("pj-t-stime").disabled = !useTime;
el("pj-t-dtime").disabled = !useTime;
el("pj-delete-task").hidden = !task;
el("pj-toggle-complete").hidden = !task;
if (task) setCompleteBtn(!!task.completed_at);
// 댓글·첨부·하위업무·연결은 기존 업무 편집 시에만
currentTaskId = task ? task.id : null;
el("pj-attach-block").hidden = !task;
@@ -1594,6 +1614,24 @@
openModal(taskModal);
}
function setCompleteBtn(done) {
const btn = el("pj-toggle-complete");
btn.textContent = done ? "↩ 완료 취소" : "✓ 완료로 표시";
btn.classList.toggle("is-done", done);
btn.dataset.completed = done ? "1" : "";
}
el("pj-toggle-complete").addEventListener("click", async function () {
const id = el("pj-t-id").value;
if (!id) return;
const done = el("pj-toggle-complete").dataset.completed === "1";
try {
const res = await api("PUT", "/project/api/tasks/" + id, { completed: !done });
mergeTask(res.task);
setCompleteBtn(!!res.task.completed_at);
refreshActiveView();
} catch (e) { alert("처리 실패: " + e.message); }
});
// ── 첨부파일 ──
function fmtSize(n) {
if (n < 1024) return n + " B";
@@ -1906,10 +1944,70 @@
function initDescEditor() {
const box = el("pj-t-desc");
if (!box) return null;
const wrap = box.parentElement;
const imgInput = el("pj-desc-img-input");
const hint = el("pj-desc-hint");
let taskId = null;
// ── 설명란 이미지 선택 → 크기 조절 핸들 + 삭제 버튼 오버레이 ──
// 크기는 style이 아니라 width 속성으로 저장한다(서버 sanitize가 style은
// 버리지만 img width/height 속성은 허용 목록에 있어 저장 후에도 유지된다).
const imgTools = document.createElement("div");
imgTools.className = "pj-img-tools";
imgTools.hidden = true;
imgTools.innerHTML =
'<button type="button" class="pj-img-del" title="이미지 삭제">×</button>' +
'<span class="pj-img-handle" title="드래그로 크기 조절"></span>';
wrap.appendChild(imgTools);
const imgDelBtn = imgTools.querySelector(".pj-img-del");
const imgHandle = imgTools.querySelector(".pj-img-handle");
let selectedImg = null;
function positionImgTools() {
if (!selectedImg || !selectedImg.isConnected) { selectedImg = null; imgTools.hidden = true; return; }
imgTools.style.left = selectedImg.offsetLeft + "px";
imgTools.style.top = selectedImg.offsetTop + "px";
imgTools.style.width = selectedImg.offsetWidth + "px";
imgTools.style.height = selectedImg.offsetHeight + "px";
imgTools.hidden = false;
}
function selectImg(img) {
if (selectedImg) selectedImg.classList.remove("pj-img-selected");
selectedImg = img || null;
if (selectedImg) { selectedImg.classList.add("pj-img-selected"); positionImgTools(); }
else { imgTools.hidden = true; }
}
box.addEventListener("click", function (e) {
if (e.target.tagName === "IMG") selectImg(e.target);
else selectImg(null);
});
document.addEventListener("click", function (e) {
if (selectedImg && !wrap.contains(e.target)) selectImg(null);
});
imgDelBtn.addEventListener("click", function () {
if (selectedImg) selectedImg.remove();
selectImg(null);
});
let dragging = false, dragStartX = 0, dragStartW = 0;
imgHandle.addEventListener("mousedown", function (e) {
if (!selectedImg) return;
e.preventDefault();
dragging = true;
dragStartX = e.clientX;
dragStartW = selectedImg.offsetWidth;
});
document.addEventListener("mousemove", function (e) {
if (!dragging || !selectedImg) return;
let newW = Math.round(dragStartW + (e.clientX - dragStartX));
newW = Math.max(40, Math.min(newW, wrap.clientWidth || 9999));
selectedImg.setAttribute("width", String(newW));
selectedImg.removeAttribute("height");
selectedImg.style.width = "";
selectedImg.style.height = "";
positionImgTools();
});
document.addEventListener("mouseup", function () { dragging = false; });
function setTaskId(id) {
taskId = id;
const enabled = !!id;
@@ -1973,7 +2071,7 @@
return {
setTaskId: setTaskId,
setHtml: function (html) { box.innerHTML = html || ""; },
setHtml: function (html) { selectImg(null); box.innerHTML = html || ""; },
getHtml: function () { return box.innerHTML; },
};
}