diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index d311d2f..2e66a3b 100644 --- a/app/modules/project/templates/project/inbox.html +++ b/app/modules/project/templates/project/inbox.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -45,5 +45,5 @@ {% endif %} - + {% endblock %} diff --git a/app/modules/project/templates/project/index.html b/app/modules/project/templates/project/index.html index 6062378..ae5c382 100644 --- a/app/modules/project/templates/project/index.html +++ b/app/modules/project/templates/project/index.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -101,5 +101,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index e6fb902..be13afb 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + @@ -119,7 +119,6 @@ {% if bar.continues_left %}pj-bar-l{% endif %} {% if bar.continues_right %}pj-bar-r{% endif %}" data-task-id="{{ bar.id }}" - {% if can_manage %}draggable="true"{% endif %} style="grid-column: {{ bar.start_col + 1 }} / span {{ bar.span }}; grid-row: {{ bar.lane + 1 }}; background: {% if bar.completed %}#9aa5b1{% else %}{{ bar.color }}{% endif %};" title="{{ bar.label }}"> @@ -257,5 +256,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index a03e80e..c58ac7a 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -137,10 +137,13 @@ .pj-bar-l { border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: 0; } .pj-bar-r { border-top-right-radius: 0; border-bottom-right-radius: 0; margin-right: 0; } .pj-bar-done { opacity: .75; text-decoration: line-through; } -/* 드래그 이동 — 드래그 중엔 bar 가 이벤트 가로채지 않게(날짜 셀이 드롭 받도록) */ +/* 드래그 이동(포인터 기반) — 드래그 중엔 bar 가 이벤트 가로채지 않게(elementFromPoint 가 날짜 셀 반환) */ +.pj-cal-dragging { cursor: grabbing; } .pj-cal-dragging .pj-bar { pointer-events: none; } -.pj-bar[draggable="true"] { cursor: grab; } +.pj-project-view[data-can-manage="true"] .pj-bar { cursor: grab; } .pj-day.pj-drop-over { background: #e6ecfa; box-shadow: inset 0 0 0 2px #aebfe6; } +/* 드래그 중 따라다니는 고스트 라벨 */ +.pj-bar-ghost { position: fixed; z-index: 2000; pointer-events: none; padding: 2px 10px; border-radius: 6px; background: #1e1f21; color: #fff; font-size: 12px; box-shadow: 0 4px 14px rgba(0,0,0,.3); white-space: nowrap; } /* ── 타임라인 컨테이너 ── */ #pj-timeline { background: #fff; border-radius: 12px; padding: 6px; } diff --git a/app/static/project.js b/app/static/project.js index 4014103..1ad1c12 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -142,22 +142,16 @@ return Math.round((new Date(b + "T00:00:00") - new Date(a + "T00:00:00")) / 86400000); } - let dragTaskId = null; - - // 한 주 안에서 clientX → 0..6 열 인덱스 → 그 칸의 날짜. - function dateAtPointer(week, clientX) { - const cells = week.querySelectorAll(".pj-week-days .pj-day[data-date]"); - if (!cells.length) return null; - const rect = week.getBoundingClientRect(); - let col = Math.floor((clientX - rect.left) / (rect.width / 7)); - col = Math.max(0, Math.min(cells.length - 1, col)); - return { date: cells[col].dataset.date, cell: cells[col] }; - } - function clearDropHL() { document.querySelectorAll(".pj-day.pj-drop-over").forEach(function (d) { d.classList.remove("pj-drop-over"); }); } + // 화면 좌표 → 그 위치의 날짜 셀(있으면). + function dayCellAt(x, y) { + const elAt = document.elementFromPoint(x, y); + return elAt ? elAt.closest(".pj-day[data-date]") : null; + } + async function moveTaskTo(taskId, dropDate) { const t = tasks.find(function (x) { return String(x.id) === String(taskId); }); if (!t) return; @@ -175,52 +169,57 @@ } catch (err) { alert("이동 실패: " + err.message); } } + // 네이티브 HTML5 드래그는 이 절대배치 grid 오버레이에서 불안정해서, + // 포인터(mousedown→mousemove→mouseup) 기반으로 직접 구현한다. function wireCalendar() { if (calendarWired) return; calendarWired = true; const cal = document.querySelector(".pj-cal"); document.querySelectorAll(".pj-bar[data-task-id]").forEach(function (bar) { - bar.addEventListener("click", function (e) { - e.preventDefault(); - if (!canManage) return; - const t = tasks.find(function (x) { return String(x.id) === bar.dataset.taskId; }); - if (t) openTaskModal(t); - }); - if (canManage) { - bar.draggable = true; - bar.addEventListener("dragstart", function (e) { - dragTaskId = bar.dataset.taskId; - e.dataTransfer.effectAllowed = "move"; - try { e.dataTransfer.setData("text/plain", bar.dataset.taskId); } catch (_) {} - if (cal) cal.classList.add("pj-cal-dragging"); - }); - bar.addEventListener("dragend", function () { - dragTaskId = null; - if (cal) cal.classList.remove("pj-cal-dragging"); - clearDropHL(); - }); - } - }); + bar.addEventListener("mousedown", function (e) { + if (e.button !== 0) return; + e.preventDefault(); // 텍스트 선택/네이티브 드래그 방지 + const taskId = bar.dataset.taskId; + const sx = e.clientX, sy = e.clientY; + let dragging = false, ghost = null; - if (!canManage) return; - // 드롭 대상은 주(week) 단위로 받고, 마우스 X 로 어느 날짜 칸인지 계산. - // (bar 오버레이/pointer-events 간섭 없이 안정적으로 드롭됨) - document.querySelectorAll(".pj-week").forEach(function (week) { - week.addEventListener("dragover", function (e) { - if (!dragTaskId) return; - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - const hit = dateAtPointer(week, e.clientX); - if (hit) { clearDropHL(); hit.cell.classList.add("pj-drop-over"); } - }); - week.addEventListener("drop", function (e) { - e.preventDefault(); - const taskId = dragTaskId || e.dataTransfer.getData("text/plain"); - clearDropHL(); - if (!taskId) return; - const hit = dateAtPointer(week, e.clientX); - if (hit) moveTaskTo(taskId, hit.date); + function onMove(ev) { + if (!dragging) { + if (Math.abs(ev.clientX - sx) + Math.abs(ev.clientY - sy) < 5) return; + dragging = true; + if (cal) cal.classList.add("pj-cal-dragging"); + if (canManage) { + ghost = document.createElement("div"); + ghost.className = "pj-bar-ghost"; + ghost.textContent = (bar.textContent || "").trim(); + document.body.appendChild(ghost); + } + } + if (ghost) { ghost.style.left = (ev.clientX + 10) + "px"; ghost.style.top = (ev.clientY + 12) + "px"; } + clearDropHL(); + const cell = dayCellAt(ev.clientX, ev.clientY); + if (cell) cell.classList.add("pj-drop-over"); + } + + function onUp(ev) { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + if (cal) cal.classList.remove("pj-cal-dragging"); + if (ghost) ghost.remove(); + clearDropHL(); + const t = tasks.find(function (x) { return String(x.id) === String(taskId); }); + if (!dragging) { // 안 움직였으면 클릭 = 모달 + if (t && canManage) openTaskModal(t); + return; + } + if (!canManage) return; + const cell = dayCellAt(ev.clientX, ev.clientY); + if (cell) moveTaskTo(taskId, cell.dataset.date); + } + + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); }); }); }