fix(project): 달력 막대 줄 순서를 주마다 일정하게 (전역 레인 + 주별 압축)
업무마다 한 달 전체 기준 고정 레인을 배정하고, 각 주는 존재하는 레인만 0,1,2…로 압축해 표시. 여러 주에 걸친 막대의 상하 순서가 주가 바뀌어도 유지됨(틱톡/쇼피 순서 뒤바뀜 수정). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/project.css?v=20260625s" />
|
<link rel="stylesheet" href="/static/project.css?v=20260625t" />
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -164,5 +164,5 @@
|
|||||||
<script>
|
<script>
|
||||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/project.js?v=20260625s" defer></script>
|
<script src="/static/project.js?v=20260625t" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/project.css?v=20260625s" />
|
<link rel="stylesheet" href="/static/project.css?v=20260625t" />
|
||||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
<!-- 타임라인 vis-timeline — self-host -->
|
<!-- 타임라인 vis-timeline — self-host -->
|
||||||
@@ -289,5 +289,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||||
<script src="/static/project.js?v=20260625s" defer></script>
|
<script src="/static/project.js?v=20260625t" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+39
-12
@@ -227,18 +227,44 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 업무마다 '한 달 전체' 기준으로 고정 레인(행) 배정 → 여러 주에 걸쳐도 같은 줄 유지.
|
||||||
|
// (주마다 따로 배치하면 첫주/다음주의 순서가 바뀌는 문제 방지)
|
||||||
|
function computeLanes() {
|
||||||
|
const items = [];
|
||||||
|
tasks.forEach(function (t) {
|
||||||
|
const sp = taskSpan(t);
|
||||||
|
if (sp) items.push({ id: t.id, s: sp[0], e: sp[1] });
|
||||||
|
});
|
||||||
|
// 시작일 → 종료일 → id 순(결정적). 문자열 'YYYY-MM-DD' 사전식 비교.
|
||||||
|
items.sort(function (a, b) {
|
||||||
|
if (a.s !== b.s) return a.s < b.s ? -1 : 1;
|
||||||
|
if (a.e !== b.e) return a.e < b.e ? -1 : 1;
|
||||||
|
return a.id - b.id;
|
||||||
|
});
|
||||||
|
const laneEnd = []; // lane 별 마지막 점유 종료일
|
||||||
|
const map = {};
|
||||||
|
items.forEach(function (it) {
|
||||||
|
for (var i = 0; i < laneEnd.length; i++) {
|
||||||
|
if (it.s > laneEnd[i]) { map[it.id] = i; laneEnd[i] = it.e; return; }
|
||||||
|
}
|
||||||
|
map[it.id] = laneEnd.length; laneEnd.push(it.e);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
// 전체 달력 다시 그리기 — 서버가 그린 날짜 셀(.pj-day[data-date])을 읽어 주별로 막대 배치.
|
// 전체 달력 다시 그리기 — 서버가 그린 날짜 셀(.pj-day[data-date])을 읽어 주별로 막대 배치.
|
||||||
function renderCalendar() {
|
function renderCalendar() {
|
||||||
const cal = document.querySelector(".pj-cal");
|
const cal = document.querySelector(".pj-cal");
|
||||||
if (!cal) return;
|
if (!cal) return;
|
||||||
closeDayPop();
|
closeDayPop();
|
||||||
|
const laneOf = computeLanes();
|
||||||
cal.querySelectorAll(".pj-week").forEach(function (weekEl) {
|
cal.querySelectorAll(".pj-week").forEach(function (weekEl) {
|
||||||
const dayEls = weekEl.querySelectorAll(".pj-day[data-date]");
|
const dayEls = weekEl.querySelectorAll(".pj-day[data-date]");
|
||||||
if (!dayEls.length) return;
|
if (!dayEls.length) return;
|
||||||
const weekStart = dayEls[0].dataset.date;
|
const weekStart = dayEls[0].dataset.date;
|
||||||
const weekEnd = dayEls[dayEls.length - 1].dataset.date;
|
const weekEnd = dayEls[dayEls.length - 1].dataset.date;
|
||||||
|
|
||||||
// 이 주에 걸치는 업무 → 막대 세그먼트
|
// 이 주에 걸치는 업무 → 막대 세그먼트(레인은 전역 배정값 사용)
|
||||||
const bars = [];
|
const bars = [];
|
||||||
tasks.forEach(function (t) {
|
tasks.forEach(function (t) {
|
||||||
const sp = taskSpan(t);
|
const sp = taskSpan(t);
|
||||||
@@ -251,23 +277,24 @@
|
|||||||
startCol: dayDiff(weekStart, segStart),
|
startCol: dayDiff(weekStart, segStart),
|
||||||
span: dayDiff(segStart, segEnd) + 1,
|
span: dayDiff(segStart, segEnd) + 1,
|
||||||
cl: sp[0] < weekStart, cr: sp[1] > weekEnd,
|
cl: sp[0] < weekStart, cr: sp[1] > weekEnd,
|
||||||
|
lane: laneOf[t.id] || 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// 레인(행) 그리디 배치
|
|
||||||
bars.sort(function (a, b) { return a.startCol - b.startCol || b.span - a.span; });
|
// 전역 레인 순서는 유지하되, 이 주에 실제 존재하는 레인만 0,1,2…로 압축
|
||||||
const laneEnd = [];
|
// (빈 행 제거 → 헛된 '+N' 방지, 단 두 막대의 상하 순서는 항상 동일).
|
||||||
bars.forEach(function (b) {
|
const present = {};
|
||||||
for (var i = 0; i < laneEnd.length; i++) {
|
bars.forEach(function (b) { present[b.lane] = true; });
|
||||||
if (b.startCol > laneEnd[i]) { b.lane = i; laneEnd[i] = b.startCol + b.span - 1; return; }
|
const lanesSorted = Object.keys(present).map(Number).sort(function (a, b) { return a - b; });
|
||||||
}
|
const rankOf = {};
|
||||||
b.lane = laneEnd.length; laneEnd.push(b.startCol + b.span - 1);
|
lanesSorted.forEach(function (l, i) { rankOf[l] = i; });
|
||||||
});
|
|
||||||
|
|
||||||
const cont = weekEl.querySelector(".pj-week-bars");
|
const cont = weekEl.querySelector(".pj-week-bars");
|
||||||
cont.innerHTML = "";
|
cont.innerHTML = "";
|
||||||
const overflow = [0, 0, 0, 0, 0, 0, 0];
|
const overflow = [0, 0, 0, 0, 0, 0, 0];
|
||||||
bars.forEach(function (b) {
|
bars.forEach(function (b) {
|
||||||
if (b.lane >= MAX_LANES) { // 넘침 → 그 칸들 +N 카운트
|
const row = rankOf[b.lane];
|
||||||
|
if (row >= MAX_LANES) { // 넘침 → 그 칸들 +N 카운트
|
||||||
for (var c = b.startCol; c < b.startCol + b.span; c++) overflow[c]++;
|
for (var c = b.startCol; c < b.startCol + b.span; c++) overflow[c]++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -276,7 +303,7 @@
|
|||||||
(b.cl ? " pj-bar-l" : "") + (b.cr ? " pj-bar-r" : "");
|
(b.cl ? " pj-bar-l" : "") + (b.cr ? " pj-bar-r" : "");
|
||||||
div.dataset.taskId = b.t.id;
|
div.dataset.taskId = b.t.id;
|
||||||
div.style.gridColumn = (b.startCol + 1) + " / span " + b.span;
|
div.style.gridColumn = (b.startCol + 1) + " / span " + b.span;
|
||||||
div.style.gridRow = (b.lane + 1);
|
div.style.gridRow = (row + 1);
|
||||||
div.style.background = b.t.completed_at ? "#9aa5b1" : (b.t.project_color || "#4573d2");
|
div.style.background = b.t.completed_at ? "#9aa5b1" : (b.t.project_color || "#4573d2");
|
||||||
div.title = "[" + (b.t.project_name || "") + "] " + (b.t.title || "") +
|
div.title = "[" + (b.t.project_name || "") + "] " + (b.t.title || "") +
|
||||||
(b.t.assignee_email ? " · " + idOf(b.t.assignee_email) : "");
|
(b.t.assignee_email ? " · " + idOf(b.t.assignee_email) : "");
|
||||||
|
|||||||
Reference in New Issue
Block a user