diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index ef446ac..2e71266 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 24aae5b..30360e9 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 4aa2966..a4b77ea 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 %} - + @@ -379,5 +379,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index a80a31b..ac2bbab 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -191,21 +191,24 @@ transform-origin: top center; } -/* ── 보드(칸반) — 4열 x 2행 그리드. 세션이 8개를 넘으면 이 영역 안에서만 +/* ── 보드(칸반) — 최대 4열 매이슨리(masonry). CSS Grid로 4열을 만들면 같은 + 행의 한 칸만 카드가 많아도 그 행 전체 높이가 맞춰져 빈 칸이 생긴다 — + 그래서 각 세션을 JS(layoutMasonry)로 절대배치해 열은 고정(왼쪽부터 + 순서대로 배정)하되 높이는 열마다 독립적으로 쌓이게 한다(같은 열의 다음 + 세션이 바로 위 세션 아래에 붙는다). 세션이 많아지면 이 영역 안에서만 세로 스크롤되고, "+ 세션 추가" 버튼은 이 영역 밖(pj-board-toolbar)에 있어 스크롤과 무관하게 항상 보인다. ── */ .pj-board-toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; } .pj-board-toolbar .pj-add-stage { width: auto; padding: 8px 18px; } .pj-board { - display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: min-content; - gap: 16px; align-content: start; max-height: 74vh; overflow-y: auto; padding-right: 4px; padding-bottom: 8px; + position: relative; max-height: 74vh; overflow-y: auto; overflow-x: hidden; + padding-right: 4px; padding-bottom: 8px; } -@media (max-width: 1400px) { .pj-board { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 1100px) { .pj-board { grid-template-columns: repeat(2, minmax(0, 1fr)); } } -@media (max-width: 700px) { .pj-board { grid-template-columns: 1fr; } } /* 세션(보드 컬럼) — 배경이 페이지와 거의 같으면 구분이 안 되므로(#f6f5fb ≈ - 흰 배경) 뚜렷한 회색 톤 + 테두리로 아사나처럼 컬럼 경계를 분명히 준다. */ -.pj-col { background: #eef0f4; border: 1px solid #e2e4ea; border-radius: 14px; padding: 12px; min-width: 0; } + 흰 배경) 뚜렷한 회색 톤 + 테두리로 아사나처럼 컬럼 경계를 분명히 준다. + position/width/left/top 은 layoutMasonry 가 매번 인라인으로 계산해 준다. */ +.pj-col { position: absolute; background: #eef0f4; border: 1px solid #e2e4ea; border-radius: 14px; padding: 12px; box-sizing: border-box; } +.pj-empty { position: static; } .pj-col-head { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 13px; padding: 2px 6px 12px; margin-bottom: 4px; color: var(--pj-ink); diff --git a/app/static/project.js b/app/static/project.js index 6f9168c..bcd1a53 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -1108,8 +1108,38 @@ if (boardAddStageBtn) boardAddStageBtn.hidden = !(viewScope === "project" && canManage); if (viewScope === "project") renderBoardByStage(board, vis); else renderBoardByName(board, vis); + layoutMasonry(board); } + // 세션(보드 컬럼) 매이슨리 배치 — 열은 왼쪽부터 순서대로 고정 배정하고, + // 각 열은 자기 안에 쌓인 카드 높이만큼만 다음 세션을 그 밑에 이어붙인다 + // (CSS Grid처럼 행 전체를 가장 큰 세션에 맞춰 늘리지 않는다). + function layoutMasonry(board) { + const items = Array.prototype.slice.call(board.children).filter(function (node) { + return node.classList.contains("pj-col"); + }); + if (!items.length) { board.style.height = "auto"; return; } + const gap = 16, minColWidth = 220, maxCols = 4; + const boardWidth = board.clientWidth || 1; + const cols = Math.max(1, Math.min(maxCols, Math.floor((boardWidth + gap) / (minColWidth + gap)))); + const colWidth = (boardWidth - gap * (cols - 1)) / cols; + const colHeights = new Array(cols).fill(0); + items.forEach(function (node, i) { + const c = i % cols; + node.style.width = colWidth + "px"; + node.style.left = (c * (colWidth + gap)) + "px"; + node.style.top = colHeights[c] + "px"; + colHeights[c] += node.offsetHeight + gap; + }); + board.style.height = Math.max.apply(null, colHeights) + "px"; + } + let boardResizeTimer = null; + window.addEventListener("resize", function () { + if (currentView !== "board") return; + clearTimeout(boardResizeTimer); + boardResizeTimer = setTimeout(function () { layoutMasonry(el("pj-board")); }, 120); + }); + function renderBoardByName(board, vis) { // 컬럼 이름 = 서버가 정렬해준 전체 세션 이름 + 세션 미지정 업무용 '미지정' let names = boardStages.slice();