diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html
index 81acc30..6426c9a 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 7d6203e..c45d5f1 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 %}
@@ -376,5 +376,5 @@
-
+
{% endblock %}
diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html
index 87b0deb..81d6b8c 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 %}
-
+
@@ -139,13 +139,17 @@
+ 최대 4열 매이슨리로 배치하고, 세션이 늘어나면 pj-board 안에서만
+ 세로 스크롤된다(좌측 프로젝트 목록·우측 내 업무와 스크롤이 서로
+ 안 얽히게 각자 독립). "+ 세션 추가" 버튼은 스크롤 영역 밖(고정
+ 위치)에 있어 항상 보인다. -->
@@ -379,5 +383,5 @@
-
+
{% endblock %}
diff --git a/app/static/project.css b/app/static/project.css
index ac2bbab..43e4af0 100644
--- a/app/static/project.css
+++ b/app/static/project.css
@@ -126,18 +126,29 @@
.pj-mytask-title { font-size: 13.5px; font-weight: 600; }
.pj-mytask-meta { margin-left: auto; font-size: 11px; color: #9aa1a9; }
-/* ── 프로젝트 페이지 레이아웃 (좌: 프로젝트 트리 / 가운데: 뷰 / 우: 내 업무) ──
- 가운데 칸에 min-width:0 이 없으면 보드에 세션이 많이 늘어날 때 grid 트랙
- 자체가 넓어져 페이지 전체가 가로 스크롤된다(overflow-x:auto 는 .pj-board
- 안에서만 작동해야 한다) — 반드시 필요한 규칙. */
-.pj-layout { display: grid; grid-template-columns: 230px 1fr 300px; gap: 22px; align-items: start; }
-.pj-main { min-width: 0; }
-.pj-project-mytasks { min-width: 0; width: 300px; }
+/* ── 프로젝트 페이지 레이아웃 (좌: 프로젝트 트리 / 가운데: 세션 등 뷰 / 우:
+ 내 업무) — 화면(=.erp-page) 자체는 스크롤하지 않고 3칸이 각자 안에서만
+ 스크롤한다(하나가 길어져도 다른 칸이 같이 스크롤되지 않게). 화면 높이를
+ 그대로 쓰기 위해 휴가/쿠팡 모듈과 같은 패턴으로 .erp-page 의 flex 자식을
+ 늘려 남은 높이를 다 차지하게 하고, 가운데 폭 제한(max-width:1550px)도
+ 이 화면에서만 풀어서 좌/우 칸이 진짜 화면 양 끝에 붙게 한다. */
+.erp-page:has(> .pj-project-view) { max-width: none; }
+.pj-project-view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
+.pj-project-view .pj-layout { flex: 1 1 auto; min-height: 0; }
+.pj-layout { display: grid; grid-template-columns: 230px 1fr 300px; gap: 22px; height: 100%; min-height: 0; align-items: stretch; }
+.pj-side { min-width: 0; min-height: 0; height: 100%; overflow-y: auto; padding-right: 4px; }
+.pj-main { min-width: 0; min-height: 0; height: 100%; display: flex; flex-direction: column; }
+.pj-project-mytasks { min-width: 0; min-height: 0; width: 300px; height: 100%; overflow-y: auto; padding-right: 4px; }
@media (max-width: 1280px) {
.pj-layout { grid-template-columns: 230px 1fr; }
.pj-project-mytasks { display: none; }
}
-@media (max-width: 900px) { .pj-layout { grid-template-columns: 1fr; } }
+@media (max-width: 900px) {
+ .erp-page:has(> .pj-project-view) { max-width: 1550px; }
+ .pj-project-view { display: block; }
+ .pj-layout { display: block; height: auto; }
+ .pj-side, .pj-main { height: auto; overflow: visible; }
+}
.pj-side-block { margin-bottom: 22px; }
.pj-side-head { display: flex; align-items: center; justify-content: space-between; font-size: 12px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 8px; }
@@ -175,7 +186,13 @@
.pj-avatar.pj-gicon-sm { width: 18px; height: 18px; }
/* ── 툴바 + 탭 ── */
-.pj-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
+.pj-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex: none; }
+/* 현재 활성 뷰(보드/리스트/달력/타임라인)가 pj-main 의 남은 세로 공간을
+ 차지 — 뷰 하나만 자체 스크롤(다른 칸과 스크롤이 서로 안 얽히게). 보드는
+ 내부에 pj-board 자체 스크롤 컨테이너가 따로 있어 여기선 넘침을 숨긴다. */
+.pj-view { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
+.pj-view[hidden] { display: none; }
+.pj-view[data-view="board"]:not([hidden]) { display: flex; flex-direction: column; overflow: hidden; }
.pj-view-tabs { display: inline-flex; background: #eef0f2; border-radius: 10px; padding: 3px; gap: 2px; }
.pj-tab { border: none; background: transparent; padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; color: #525860; cursor: pointer; }
.pj-tab.is-active { background: var(--pj-accent); color: #fff; box-shadow: 0 1px 3px rgba(123,104,238,.35); }
@@ -198,12 +215,14 @@
세션이 바로 위 세션 아래에 붙는다). 세션이 많아지면 이 영역 안에서만
세로 스크롤되고, "+ 세션 추가" 버튼은 이 영역 밖(pj-board-toolbar)에
있어 스크롤과 무관하게 항상 보인다. ── */
-.pj-board-toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; }
+.pj-board-toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; flex: none; }
.pj-board-toolbar .pj-add-stage { width: auto; padding: 8px 18px; }
-.pj-board {
- position: relative; max-height: 74vh; overflow-y: auto; overflow-x: hidden;
- padding-right: 4px; padding-bottom: 8px;
-}
+/* pj-board = 스크롤 뷰포트(남은 세로 공간을 그대로 씀, 부모가 이미 flex로
+ 높이를 정해준다). pj-board-canvas = 매이슨리 절대배치 캔버스 — 스크롤
+ 컨테이너 자체 높이와 콘텐츠(캔버스) 높이 계산을 분리해야 스크롤이
+ 올바르게 생긴다(캔버스만 커지고 뷰포트는 flex 크기를 유지). */
+.pj-board { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding-right: 4px; padding-bottom: 8px; }
+.pj-board-canvas { position: relative; }
/* 세션(보드 컬럼) — 배경이 페이지와 거의 같으면 구분이 안 되므로(#f6f5fb ≈
흰 배경) 뚜렷한 회색 톤 + 테두리로 아사나처럼 컬럼 경계를 분명히 준다.
position/width/left/top 은 layoutMasonry 가 매번 인라인으로 계산해 준다. */
diff --git a/app/static/project.js b/app/static/project.js
index b786432..85f0dee 100644
--- a/app/static/project.js
+++ b/app/static/project.js
@@ -1102,13 +1102,16 @@
}
function renderBoard() {
- const board = el("pj-board");
- board.innerHTML = "";
+ // pj-board(바깥)는 overflow-y:auto 스크롤 뷰포트, pj-board-canvas(안쪽)가
+ // 실제 매이슨리 절대배치 캔버스 — 스크롤 컨테이너와 콘텐츠 높이 계산을
+ // 분리해야 pj-board 의 높이가 부모(flex) 크기를 그대로 따라간다.
+ const canvas = el("pj-board-canvas");
+ canvas.innerHTML = "";
const vis = visibleTasks();
if (boardAddStageBtn) boardAddStageBtn.hidden = !(viewScope === "project" && canManage);
- if (viewScope === "project") renderBoardByStage(board, vis);
- else renderBoardByName(board, vis);
- layoutMasonry(board);
+ if (viewScope === "project") renderBoardByStage(canvas, vis);
+ else renderBoardByName(canvas, vis);
+ layoutMasonry(canvas);
}
// 세션(보드 컬럼) 매이슨리 배치 — 열은 왼쪽부터 순서대로 고정 배정하고,
@@ -1137,7 +1140,7 @@
window.addEventListener("resize", function () {
if (currentView !== "board") return;
clearTimeout(boardResizeTimer);
- boardResizeTimer = setTimeout(function () { layoutMasonry(el("pj-board")); }, 120);
+ boardResizeTimer = setTimeout(function () { layoutMasonry(el("pj-board-canvas")); }, 120);
});
function renderBoardByName(board, vis) {
@@ -2050,6 +2053,29 @@
positionImgTools();
});
document.addEventListener("mouseup", function () { dragging = false; });
+ document.addEventListener("keydown", function (e) {
+ if (!selectedImg) return;
+ if (e.key !== "Delete" && e.key !== "Backspace") return;
+ const active = document.activeElement;
+ if (active && ["INPUT", "TEXTAREA", "SELECT"].indexOf(active.tagName) >= 0) return;
+ e.preventDefault();
+ selectedImg.remove();
+ selectImg(null);
+ });
+
+ // 업로드 버튼 클릭 → 파일 선택창이 뜨는 동안 contenteditable의 선택 범위가
+ // 풀린다 — 마지막으로 박스 안에 있던 커서 위치를 기억해뒀다가 삽입 직전에
+ // 되살린다(안 그러면 이미지가 항상 맨 끝에 붙어버린다).
+ let lastRange = null;
+ function saveSelection() {
+ const sel = window.getSelection();
+ if (sel && sel.rangeCount && box.contains(sel.anchorNode)) {
+ lastRange = sel.getRangeAt(0).cloneRange();
+ }
+ }
+ box.addEventListener("keyup", saveSelection);
+ box.addEventListener("mouseup", saveSelection);
+ box.addEventListener("blur", saveSelection);
function setTaskId(id) {
taskId = id;
@@ -2063,10 +2089,13 @@
function insertHtmlAtCursor(html) {
box.focus();
+ const sel = window.getSelection();
+ if (lastRange && (!sel.rangeCount || !box.contains(sel.anchorNode))) {
+ try { sel.removeAllRanges(); sel.addRange(lastRange); } catch (_) { lastRange = null; }
+ }
let inserted = false;
try { inserted = document.execCommand("insertHTML", false, html); } catch (_) { inserted = false; }
if (!inserted) {
- const sel = window.getSelection();
if (sel && sel.rangeCount && box.contains(sel.anchorNode)) {
const range = sel.getRangeAt(0);
range.deleteContents();
@@ -2077,6 +2106,7 @@
box.innerHTML += html;
}
}
+ saveSelection();
}
// 삽입 전 원본 크기를 확인해, 설명란보다 넓으면 90%로 줄여서 넣는다
@@ -2133,7 +2163,7 @@
return {
setTaskId: setTaskId,
- setHtml: function (html) { selectImg(null); box.innerHTML = html || ""; },
+ setHtml: function (html) { selectImg(null); lastRange = null; box.innerHTML = html || ""; },
getHtml: function () { return box.innerHTML; },
};
}