feat(project): 댓글 있는 업무에 말풍선 아이콘 표시(달력·타임라인·보드·리스트)
- list_tasks/get_task 에 comment_count 서브쿼리 추가 - 달력 막대/타임라인 항목/보드 카드/리스트 업무열에 댓글 수 말풍선 - 댓글 로드 시 메모리 카운트 동기화 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -297,7 +297,8 @@ class ProjectStore:
|
||||
with self._pool.connection() as conn:
|
||||
rows = conn.execute(
|
||||
f"SELECT t.*, p.name AS project_name, p.color AS project_color, "
|
||||
f" s.name AS stage_name, s.is_done_stage "
|
||||
f" s.name AS stage_name, s.is_done_stage, "
|
||||
f" (SELECT COUNT(*) FROM task_comments c WHERE c.task_id = t.id) AS comment_count "
|
||||
f"FROM tasks t "
|
||||
f"JOIN projects p ON p.id = t.project_id "
|
||||
f"LEFT JOIN project_stages s ON s.id = t.stage_id "
|
||||
@@ -311,7 +312,8 @@ class ProjectStore:
|
||||
with self._pool.connection() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT t.*, p.name AS project_name, p.color AS project_color, "
|
||||
" s.name AS stage_name, s.is_done_stage "
|
||||
" s.name AS stage_name, s.is_done_stage, "
|
||||
" (SELECT COUNT(*) FROM task_comments c WHERE c.task_id = t.id) AS comment_count "
|
||||
"FROM tasks t JOIN projects p ON p.id = t.project_id "
|
||||
"LEFT JOIN project_stages s ON s.id = t.stage_id "
|
||||
"WHERE t.id = %s",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260625w" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260625x" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -167,5 +167,5 @@
|
||||
<script>
|
||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||
</script>
|
||||
<script src="/static/project.js?v=20260625w" defer></script>
|
||||
<script src="/static/project.js?v=20260625x" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260625w" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260625x" />
|
||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
<!-- 타임라인 vis-timeline — self-host -->
|
||||
@@ -290,5 +290,5 @@
|
||||
</script>
|
||||
|
||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||
<script src="/static/project.js?v=20260625w" defer></script>
|
||||
<script src="/static/project.js?v=20260625x" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -286,6 +286,12 @@
|
||||
/* 보드 카드 — 전체 뷰에서 어느 프로젝트인지 배지 표시 */
|
||||
.pj-card-proj { font-size: 11px; font-weight: 700; margin-bottom: 3px; letter-spacing: -.2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* 댓글 말풍선 표시 — 색은 currentColor 상속 */
|
||||
.material-symbols-outlined.pj-cmt-ic { font-size: 14px; vertical-align: middle; margin-left: 4px; line-height: 1; }
|
||||
.material-symbols-outlined.pj-cmt-ic.pj-cmt-ic-tl { font-size: 12px; margin-left: 3px; }
|
||||
.pj-cmt-n { font-size: 11px; font-weight: 700; vertical-align: middle; margin-left: 1px; }
|
||||
.pj-card-cmt { display: inline-flex; align-items: center; color: #6b7280; }
|
||||
|
||||
/* ── 보드 단계 컨트롤 ── */
|
||||
.pj-col-head { position: relative; }
|
||||
.pj-col-name { font-weight: 700; }
|
||||
|
||||
+16
-3
@@ -175,6 +175,13 @@
|
||||
return !!cb && cb === me;
|
||||
}
|
||||
const DELETE_DENIED = "삭제는 만든 사람만 할 수 있습니다.";
|
||||
// 댓글 있으면 말풍선 아이콘(+개수). 색은 currentColor 상속(막대=흰, 카드/리스트=진회색).
|
||||
function cmtBadge(t, cls) {
|
||||
const n = t.comment_count || 0;
|
||||
if (!n) return "";
|
||||
return '<span class="material-symbols-outlined pj-cmt-ic ' + (cls || "") +
|
||||
'" title="댓글 ' + n + '개">chat_bubble</span><span class="pj-cmt-n">' + n + "</span>";
|
||||
}
|
||||
|
||||
const dataEl = el("pj-data");
|
||||
const data = JSON.parse(dataEl.textContent);
|
||||
@@ -326,7 +333,7 @@
|
||||
div.style.background = b.t.completed_at ? "#9aa5b1" : (b.t.project_color || "#4573d2");
|
||||
div.title = "[" + (b.t.project_name || "") + "] " + (b.t.title || "") +
|
||||
(b.t.assignee_email ? " · " + idOf(b.t.assignee_email) : "");
|
||||
div.innerHTML = '<span class="pj-bar-label">' + escapeHtml(b.t.title || "") + "</span>";
|
||||
div.innerHTML = '<span class="pj-bar-label">' + escapeHtml(b.t.title || "") + "</span>" + cmtBadge(b.t);
|
||||
cont.appendChild(div);
|
||||
});
|
||||
for (var col = 0; col < 7; col++) {
|
||||
@@ -484,7 +491,7 @@
|
||||
const endISO = e + (t.due_time ? "T" + t.due_time : "");
|
||||
const isRange = (e !== s) || (!!t.start_time && !!t.due_time && t.start_time !== t.due_time);
|
||||
items.push({
|
||||
id: t.id, group: grp, content: t.title,
|
||||
id: t.id, group: grp, content: escapeHtml(t.title || "") + cmtBadge(t, "pj-cmt-ic-tl"),
|
||||
start: startISO, end: isRange ? endISO : undefined,
|
||||
type: isRange ? "range" : "point",
|
||||
style: "background-color:" + (t.completed_at ? "#9aa5b1" : projColor) + ";color:#fff;border:none;",
|
||||
@@ -613,6 +620,7 @@
|
||||
escapeHtml(idOf(t.assignee_email)) + "</span>"
|
||||
: "") +
|
||||
(t.due_date ? '<span class="pj-card-due">' + t.due_date + "</span>" : "") +
|
||||
(t.comment_count ? '<span class="pj-card-cmt">' + cmtBadge(t) + "</span>" : "") +
|
||||
"</div>";
|
||||
card.addEventListener("click", function () { if (canManage) openTaskModal(t); });
|
||||
if (canManage) {
|
||||
@@ -724,7 +732,9 @@
|
||||
const pc = t.project_color || "#4573d2";
|
||||
tr.style.background = pc + "14";
|
||||
tr.innerHTML = LIST_COLS.map(function (c) {
|
||||
return "<td>" + escapeHtml(String(c.text(t))) + "</td>";
|
||||
let inner = escapeHtml(String(c.text(t)));
|
||||
if (c.key === "title") inner += cmtBadge(t);
|
||||
return "<td>" + inner + "</td>";
|
||||
}).join("");
|
||||
if (canManage) { tr.style.cursor = "pointer"; tr.addEventListener("click", function () { openTaskModal(t); }); }
|
||||
tbody.appendChild(tr);
|
||||
@@ -833,6 +843,9 @@
|
||||
ul.innerHTML = "<li class='pj-empty-sm'>불러오는 중…</li>";
|
||||
try {
|
||||
const res = await api("GET", "/project/api/tasks/" + taskId + "/comments");
|
||||
// 메모리 업무의 댓글 수 동기화(아이콘 표시용)
|
||||
const tt = tasks.find(function (x) { return String(x.id) === String(taskId); });
|
||||
if (tt) tt.comment_count = res.comments.length;
|
||||
if (!res.comments.length) { ul.innerHTML = "<li class='pj-empty-sm'>댓글 없음</li>"; return; }
|
||||
ul.innerHTML = "";
|
||||
res.comments.forEach(function (c) {
|
||||
|
||||
Reference in New Issue
Block a user