feat(project): 완료 프로젝트를 "완료 프로젝트" 별도 섹션으로 다시 분류
같은 그리드 안에서 끝으로만 보내던 것을 되돌려, 진행중/완료를 다시 분리된 섹션(그리드)으로 나눔. 완료 체크 시 카드가 "완료 프로젝트" 섹션으로 옮겨지고(반투명은 기존 .is-done 유지), 해제하면 "진행중인 프로젝트"로 복귀 — 각 섹션 안에서는 sort_order 기준 정렬 유지. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -389,11 +389,10 @@ async def index(request: Request) -> HTMLResponse:
|
|||||||
member_email=None if admin else user["email"],
|
member_email=None if admin else user["email"],
|
||||||
)
|
)
|
||||||
tree = _build_tree(projects)
|
tree = _build_tree(projects)
|
||||||
# 완료 프로젝트는 별도 섹션 대신, 같은 대시보드 안에서 반투명 + 맨
|
# 진행중/완료 프로젝트를 별도 섹션으로 분류. list_projects 가 이미
|
||||||
# 끝으로만 보낸다(세션 완료 처리와 같은 원칙). sorted() 는 안정 정렬이라
|
# sort_order 순으로 내려주므로 필터링만 해도 각 섹션 내부 순서는 그대로.
|
||||||
# tree 가 이미 sort_order 순이면 완료 여부로만 다시 나눠도 각 그룹
|
active_projects = [p for p in tree if p.get("status") != "completed"]
|
||||||
# 내부 순서는 그대로 유지된다 — 완료 해제 시 원래 자리로 자동 복귀.
|
done_projects = [p for p in tree if p.get("status") == "completed"]
|
||||||
home_projects = sorted(tree, key=lambda p: p.get("status") == "completed")
|
|
||||||
|
|
||||||
# 아바타 맵: 이메일(소문자) → 구글 프로필 이미지 URL (홈 카드 업무 담당자 표시용)
|
# 아바타 맵: 이메일(소문자) → 구글 프로필 이미지 URL (홈 카드 업무 담당자 표시용)
|
||||||
from app.main import user_store # noqa: WPS433
|
from app.main import user_store # noqa: WPS433
|
||||||
@@ -475,7 +474,8 @@ async def index(request: Request) -> HTMLResponse:
|
|||||||
"page_title": "프로젝트 관리",
|
"page_title": "프로젝트 관리",
|
||||||
"page_subtitle": "달력·타임라인·보드로 프로젝트를 관리하세요.",
|
"page_subtitle": "달력·타임라인·보드로 프로젝트를 관리하세요.",
|
||||||
"force_sidebar_collapsed": True,
|
"force_sidebar_collapsed": True,
|
||||||
"home_projects": home_projects,
|
"active_projects": active_projects,
|
||||||
|
"done_projects": done_projects,
|
||||||
"my_projects": my_projects,
|
"my_projects": my_projects,
|
||||||
"avatars": avatars,
|
"avatars": avatars,
|
||||||
"home_tasks": home_tasks,
|
"home_tasks": home_tasks,
|
||||||
|
|||||||
@@ -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=20260918b" />
|
<link rel="stylesheet" href="/static/project.css?v=20260918c" />
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -45,5 +45,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/project.js?v=20260918b" defer></script>
|
<script src="/static/project.js?v=20260918c" 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=20260918b" />
|
<link rel="stylesheet" href="/static/project.css?v=20260918c" />
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -125,19 +125,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not home_projects %}
|
{% if not active_projects and not done_projects %}
|
||||||
<div class="pj-empty">
|
<div class="pj-empty">
|
||||||
아직 프로젝트가 없습니다. 상단의 <b>새 프로젝트</b> 버튼으로 만들어 보세요.
|
아직 프로젝트가 없습니다. 상단의 <b>새 프로젝트</b> 버튼으로 만들어 보세요.
|
||||||
</div>
|
</div>
|
||||||
|
{% elif not active_projects %}
|
||||||
|
<div class="pj-empty pj-empty-sm">진행중인 프로젝트가 없습니다.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- 완료 프로젝트는 별도 섹션이 아니라 같은 그리드 안에서 반투명 +
|
|
||||||
맨 끝으로만(세션 완료 처리와 같은 원칙). 서버가 이미 정렬해서
|
|
||||||
내려주고(완료 여부로만 안정 정렬), 완료 토글 시 JS가 같은
|
|
||||||
기준으로 카드를 다시 배치한다(resortProjectGrid). -->
|
|
||||||
<div class="pj-project-grid" id="pj-project-grid">
|
<div class="pj-project-grid" id="pj-project-grid">
|
||||||
{% for p in home_projects %}{{ project_card(p) }}{% endfor %}
|
{% for p in active_projects %}{{ project_card(p) }}{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if done_projects %}
|
||||||
|
<div class="pj-section-head pj-section-head-done">
|
||||||
|
<h2>완료 프로젝트</h2>
|
||||||
|
</div>
|
||||||
|
<div class="pj-project-grid" id="pj-project-grid-done">
|
||||||
|
{% for p in done_projects %}{{ project_card(p) }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 내 업무 -->
|
<!-- 내 업무 -->
|
||||||
@@ -421,5 +428,5 @@
|
|||||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||||
window.PJ_STAGE_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772","#e8398a","#a15c43"] | tojson }};
|
window.PJ_STAGE_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772","#e8398a","#a15c43"] | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/project.js?v=20260918b" defer></script>
|
<script src="/static/project.js?v=20260918c" 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=20260918b" />
|
<link rel="stylesheet" href="/static/project.css?v=20260918c" />
|
||||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — 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 -->
|
||||||
@@ -544,5 +544,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=20260918b" defer></script>
|
<script src="/static/project.js?v=20260918c" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
.pj-icon-btn:hover { background: #eceef0; color: #1e1f21; }
|
.pj-icon-btn:hover { background: #eceef0; color: #1e1f21; }
|
||||||
|
|
||||||
.pj-section-head { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 14px; }
|
.pj-section-head { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 14px; }
|
||||||
|
.pj-section-head-done { margin-top: 22px; }
|
||||||
|
.pj-section-head-done h2 { font-size: 14px; color: #6b7280; }
|
||||||
.pj-section-head h2 { font-size: 16px; font-weight: 700; margin: 0; }
|
.pj-section-head h2 { font-size: 16px; font-weight: 700; margin: 0; }
|
||||||
|
|
||||||
.pj-empty { padding: 28px; text-align: center; color: #6b7280; background: #f7f8f9; border-radius: 12px; }
|
.pj-empty { padding: 28px; text-align: center; color: #6b7280; background: #f7f8f9; border-radius: 12px; }
|
||||||
|
|||||||
+14
-10
@@ -570,25 +570,29 @@
|
|||||||
}
|
}
|
||||||
} else if (dueChip) dueChip.remove();
|
} else if (dueChip) dueChip.remove();
|
||||||
}
|
}
|
||||||
// 완료 체크 시 반투명 + 대시보드 맨 끝으로, 해제하면 sort_order 기준
|
// 완료 체크 시 "완료 프로젝트" 섹션으로, 해제하면 "진행중인 프로젝트"로
|
||||||
// 원래 자리로(세션 완료 처리와 같은 원칙 — resortProjectGrid 가 매번
|
// 되돌린다. 각 섹션 안에서는 sort_order 기준 안정 정렬 — 드래그로 손댄
|
||||||
// "완료 여부 → sort_order → id" 순으로 안정 재배치한다).
|
// 적 없으면 원래 순서 그대로 유지된다.
|
||||||
resortProjectGrid();
|
moveProjectCardToSection(card, p.status === "completed");
|
||||||
}
|
}
|
||||||
function resortProjectGrid() {
|
function sortGridByOrder(grid) {
|
||||||
const grid = el("pj-project-grid");
|
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
const cards = Array.prototype.slice.call(grid.querySelectorAll(".pj-project-card"));
|
const cards = Array.prototype.slice.call(grid.querySelectorAll(".pj-project-card"));
|
||||||
cards.sort(function (a, b) {
|
cards.sort(function (a, b) {
|
||||||
const da = a.dataset.status === "completed" ? 1 : 0;
|
|
||||||
const db = b.dataset.status === "completed" ? 1 : 0;
|
|
||||||
if (da !== db) return da - db;
|
|
||||||
const oa = Number(a.dataset.sortOrder) || 0, ob = Number(b.dataset.sortOrder) || 0;
|
const oa = Number(a.dataset.sortOrder) || 0, ob = Number(b.dataset.sortOrder) || 0;
|
||||||
if (oa !== ob) return oa - ob;
|
if (oa !== ob) return oa - ob;
|
||||||
return Number(a.dataset.id) - Number(b.dataset.id);
|
return Number(a.dataset.id) - Number(b.dataset.id);
|
||||||
});
|
});
|
||||||
cards.forEach(function (c) { grid.appendChild(c); });
|
cards.forEach(function (c) { grid.appendChild(c); });
|
||||||
}
|
}
|
||||||
|
function moveProjectCardToSection(card, isDone) {
|
||||||
|
// "완료 프로젝트" 섹션이 화면에 아직 없으면(이번이 첫 완료 프로젝트)
|
||||||
|
// 다음 새로고침까지 원래 자리에 남는다 — 드문 경우라 허용.
|
||||||
|
const targetGrid = el(isDone ? "pj-project-grid-done" : "pj-project-grid");
|
||||||
|
if (!targetGrid) return;
|
||||||
|
targetGrid.appendChild(card);
|
||||||
|
sortGridByOrder(targetGrid);
|
||||||
|
}
|
||||||
// 새 프로젝트 카드 DOM — project_card(p) 매크로(index.html)를 그대로 미러링.
|
// 새 프로젝트 카드 DOM — project_card(p) 매크로(index.html)를 그대로 미러링.
|
||||||
// 방금 만든 프로젝트라 멤버/업무는 아직 없다(멤버는 생성 직후 별도 배정,
|
// 방금 만든 프로젝트라 멤버/업무는 아직 없다(멤버는 생성 직후 별도 배정,
|
||||||
// 업무는 카드의 "업무 추가"로 나중에). 만든 사람은 자동 owner 라 관리 가능.
|
// 업무는 카드의 "업무 추가"로 나중에). 만든 사람은 자동 owner 라 관리 가능.
|
||||||
@@ -1343,7 +1347,7 @@
|
|||||||
// 프로젝트 카드 드래그 순서 변경 — 진행중/완료 그리드 각각 안에서만
|
// 프로젝트 카드 드래그 순서 변경 — 진행중/완료 그리드 각각 안에서만
|
||||||
// 재정렬(그리드 사이 드래그로 옮기면 상태가 바뀐 것처럼 보이니 막는다).
|
// 재정렬(그리드 사이 드래그로 옮기면 상태가 바뀐 것처럼 보이니 막는다).
|
||||||
function wireProjectGridDrag() {
|
function wireProjectGridDrag() {
|
||||||
const grids = [el("pj-project-grid")].filter(Boolean);
|
const grids = [el("pj-project-grid"), el("pj-project-grid-done")].filter(Boolean);
|
||||||
if (!grids.length) return;
|
if (!grids.length) return;
|
||||||
let dragCard = null, sourceGrid = null;
|
let dragCard = null, sourceGrid = null;
|
||||||
grids.forEach(function (grid) {
|
grids.forEach(function (grid) {
|
||||||
|
|||||||
Reference in New Issue
Block a user