feat(project): 댓글 팝업 고정크기 500x850·업무제목 앞 상태아이콘→댓글아이콘 정리

- 댓글 팝업 제목을 업무 제목으로 표시, 우상단 닫기(X) 버튼 추가
- 팝업 크기 500x850 고정, 내용 많으면 목록만 스크롤(팝업 크기 유지)
- 카드/내 업무 목록의 완료여부 원형 아이콘 제거, 그 자리를 댓글 아이콘 전용 슬롯으로 교체
- 댓글 유무와 무관하게 슬롯 폭을 고정해 업무 제목이 항상 같은 위치에서 시작(세로 정렬)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:22:50 +09:00
parent f54344c3ef
commit b3c98995c9
5 changed files with 49 additions and 28 deletions
@@ -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=20260915f" /> <link rel="stylesheet" href="/static/project.css?v=20260915g" />
<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=20260915e" defer></script> <script src="/static/project.js?v=20260915f" 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=20260915f" /> <link rel="stylesheet" href="/static/project.css?v=20260915g" />
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
{% endblock %} {% endblock %}
@@ -68,7 +68,13 @@
{% for t in p.task_preview %} {% for t in p.task_preview %}
<li class="pj-project-taskrow {% if t.completed_at %}is-done{% endif %}"> <li class="pj-project-taskrow {% if t.completed_at %}is-done{% endif %}">
<a href="/project/p/{{ p.id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link"> <a href="/project/p/{{ p.id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link">
<span class="material-symbols-outlined pj-project-task-ic">{% if t.completed_at %}task_alt{% else %}radio_button_unchecked{% endif %}</span> <span class="pj-project-task-lead">
{% if t.comment_count %}
<span class="pj-cmt-badge" data-task-id="{{ t.id }}" data-count="{{ t.comment_count }}" title="댓글 {{ t.comment_count }}개">
<span class="pj-cmt-ic">💬</span><span class="pj-cmt-n">{{ t.comment_count }}</span>
</span>
{% endif %}
</span>
<span class="pj-project-task-title">{{ t.title }}</span> <span class="pj-project-task-title">{{ t.title }}</span>
{% if t.stage_name %}<span class="pj-project-task-stage">{{ t.stage_name }}</span>{% endif %} {% if t.stage_name %}<span class="pj-project-task-stage">{{ t.stage_name }}</span>{% endif %}
{% if t.assignee_display %} {% if t.assignee_display %}
@@ -82,11 +88,6 @@
{% if t.due_date %} {% if t.due_date %}
<span class="pj-project-task-due {% if t.due_overdue %}is-overdue{% endif %}">{{ t.due_label }}</span> <span class="pj-project-task-due {% if t.due_overdue %}is-overdue{% endif %}">{{ t.due_label }}</span>
{% endif %} {% endif %}
{% if t.comment_count %}
<span class="pj-cmt-badge" data-task-id="{{ t.id }}" data-count="{{ t.comment_count }}" title="댓글 {{ t.comment_count }}개">
<span class="pj-cmt-ic">💬</span><span class="pj-cmt-n">{{ t.comment_count }}</span>
</span>
{% endif %}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
@@ -162,7 +163,13 @@
{% for t in p.tasks %} {% for t in p.tasks %}
<li class="pj-mytree-task {% if t.completed_at %}is-done{% endif %}"> <li class="pj-mytree-task {% if t.completed_at %}is-done{% endif %}">
<a href="/project/p/{{ t.project_id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link"> <a href="/project/p/{{ t.project_id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link">
<span class="material-symbols-outlined pj-mytree-ic" style="color: {{ t.project_color }};">{% if t.completed_at %}task_alt{% else %}radio_button_unchecked{% endif %}</span> <span class="pj-mytree-lead">
{% if t.comment_count %}
<span class="pj-cmt-badge" data-task-id="{{ t.id }}" data-count="{{ t.comment_count }}" title="댓글 {{ t.comment_count }}개">
<span class="pj-cmt-ic">💬</span><span class="pj-cmt-n">{{ t.comment_count }}</span>
</span>
{% endif %}
</span>
<span class="pj-mytree-body"> <span class="pj-mytree-body">
<span class="pj-mytree-title">{{ t.title }}</span> <span class="pj-mytree-title">{{ t.title }}</span>
{% if t.start_date or t.due_date %} {% if t.start_date or t.due_date %}
@@ -173,11 +180,6 @@
</span> </span>
{% endif %} {% endif %}
</span> </span>
{% if t.comment_count %}
<span class="pj-cmt-badge" data-task-id="{{ t.id }}" data-count="{{ t.comment_count }}" title="댓글 {{ t.comment_count }}개">
<span class="pj-cmt-ic">💬</span><span class="pj-cmt-n">{{ t.comment_count }}</span>
</span>
{% endif %}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
@@ -307,7 +309,12 @@
<!-- 댓글만 보는 팝업 (업무 카드의 말풍선 아이콘 더블클릭) --> <!-- 댓글만 보는 팝업 (업무 카드의 말풍선 아이콘 더블클릭) -->
<div class="pj-modal" id="pj-modal-comments" hidden> <div class="pj-modal" id="pj-modal-comments" hidden>
<div class="pj-modal-card pj-modal-chat"> <div class="pj-modal-card pj-modal-chat">
<h3>댓글</h3> <div class="pj-chat-head">
<h3 id="pj-cm-title">댓글</h3>
<button type="button" class="pj-icon-btn" data-close title="닫기">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<input type="hidden" id="pj-cm-task-id" /> <input type="hidden" id="pj-cm-task-id" />
<ul class="pj-chat-list" id="pj-chat-list"></ul> <ul class="pj-chat-list" id="pj-chat-list"></ul>
<div class="pj-comment-form"> <div class="pj-comment-form">
@@ -323,5 +330,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=20260915e" defer></script> <script src="/static/project.js?v=20260915f" 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=20260915f" /> <link rel="stylesheet" href="/static/project.css?v=20260915g" />
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — 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 -->
@@ -291,5 +291,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=20260915e" defer></script> <script src="/static/project.js?v=20260915f" defer></script>
{% endblock %} {% endblock %}
+13 -5
View File
@@ -73,7 +73,8 @@
text-decoration: none; color: inherit; text-decoration: none; color: inherit;
} }
.pj-project-taskrow a:hover { background: #f6f7f8; } .pj-project-taskrow a:hover { background: #f6f7f8; }
.pj-project-task-ic { font-size: 16px; flex: 0 0 auto; color: #b7bdc5; } /* 댓글 아이콘 자리 — 댓글이 없어도 폭을 고정해 제목이 항상 같은 위치에서 시작(세로 정렬)되게 한다. */
.pj-project-task-lead { flex: 0 0 32px; display: inline-flex; align-items: center; }
.pj-project-task-title { .pj-project-task-title {
font-size: 12.5px; font-weight: 600; flex: 1; min-width: 0; font-size: 12.5px; font-weight: 600; flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -90,7 +91,6 @@
.pj-project-task-due { font-size: 11px; color: #9aa1a9; flex: 0 0 auto; } .pj-project-task-due { font-size: 11px; color: #9aa1a9; flex: 0 0 auto; }
.pj-project-task-due.is-overdue { color: #c22b10; font-weight: 700; } .pj-project-task-due.is-overdue { color: #c22b10; font-weight: 700; }
.pj-project-taskrow.is-done .pj-project-task-title { text-decoration: line-through; color: #9aa1a9; } .pj-project-taskrow.is-done .pj-project-task-title { text-decoration: line-through; color: #9aa1a9; }
.pj-project-taskrow.is-done .pj-project-task-ic { color: #9aa5b1; }
.pj-project-more { .pj-project-more {
display: block; padding: 6px 16px 12px; font-size: 12px; font-weight: 600; display: block; padding: 6px 16px 12px; font-size: 12px; font-weight: 600;
color: #6b7280; text-decoration: none; color: #6b7280; text-decoration: none;
@@ -210,7 +210,8 @@
.pj-mytree-tasks { list-style: none; margin: 2px 0 0; padding: 0 0 0 8px; border-left: 2px solid #eef0f2; margin-left: 10px; } .pj-mytree-tasks { list-style: none; margin: 2px 0 0; padding: 0 0 0 8px; border-left: 2px solid #eef0f2; margin-left: 10px; }
.pj-mytree-task a { display: flex; align-items: flex-start; gap: 7px; padding: 6px 6px; border-radius: 7px; text-decoration: none; color: inherit; } .pj-mytree-task a { display: flex; align-items: flex-start; gap: 7px; padding: 6px 6px; border-radius: 7px; text-decoration: none; color: inherit; }
.pj-mytree-task a:hover { background: #f6f7f8; } .pj-mytree-task a:hover { background: #f6f7f8; }
.pj-mytree-ic { font-size: 16px; flex: 0 0 auto; margin-top: 1px; } /* 댓글 아이콘 자리 — 댓글이 없어도 폭을 고정해 제목이 항상 같은 위치에서 시작(세로 정렬)되게 한다. */
.pj-mytree-lead { flex: 0 0 32px; display: inline-flex; align-items: center; margin-top: 1px; }
.pj-mytree-body { display: flex; flex-direction: column; min-width: 0; } .pj-mytree-body { display: flex; flex-direction: column; min-width: 0; }
.pj-mytree-title { font-size: 13px; font-weight: 600; } .pj-mytree-title { font-size: 13px; font-weight: 600; }
.pj-mytree-dates { font-size: 11px; color: #9aa1a9; margin-top: 1px; } .pj-mytree-dates { font-size: 11px; color: #9aa1a9; margin-top: 1px; }
@@ -351,8 +352,15 @@
.pj-member-pick input { width: auto !important; margin: 0 !important; } .pj-member-pick input { width: auto !important; margin: 0 !important; }
/* ── 댓글만 보는 채팅형 팝업(카톡 대화창 느낌) ── */ /* ── 댓글만 보는 채팅형 팝업(카톡 대화창 느낌) ── */
.pj-modal-chat { width: min(420px, 92vw); display: flex; flex-direction: column; } /* 크기 고정(500x850) — 내용이 많아도 팝업 크기는 그대로, 목록만 스크롤. */
.pj-chat-list { list-style: none; margin: 0 0 10px; padding: 4px; max-height: 420px; min-height: 120px; overflow-y: auto; background: #eef0f2; border-radius: 10px; display: flex; flex-direction: column; gap: 8px; } .pj-modal-chat {
width: 500px; height: 850px; max-width: 96vw; max-height: 92vh;
box-sizing: border-box; display: flex; flex-direction: column;
}
.pj-chat-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; flex: 0 0 auto; }
.pj-chat-head h3 { margin: 0; font-size: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pj-chat-list { list-style: none; margin: 0 0 10px; padding: 4px; flex: 1 1 auto; min-height: 0; overflow-y: auto; background: #eef0f2; border-radius: 10px; display: flex; flex-direction: column; gap: 8px; }
.pj-modal-chat .pj-comment-form { flex: 0 0 auto; }
.pj-chat-msg { display: flex; flex-direction: column; align-items: flex-start; max-width: 78%; } .pj-chat-msg { display: flex; flex-direction: column; align-items: flex-start; max-width: 78%; }
.pj-chat-msg.mine { align-self: flex-end; align-items: flex-end; } .pj-chat-msg.mine { align-self: flex-end; align-items: flex-end; }
.pj-chat-name { font-size: 11px; color: #6b7280; margin-bottom: 2px; padding: 0 2px; } .pj-chat-name { font-size: 11px; color: #6b7280; margin-bottom: 2px; padding: 0 2px; }
+10 -4
View File
@@ -190,13 +190,17 @@
}); });
} }
// 카드/내 업무 클릭 팝업 + 댓글 팝업 공용 — 업무 원본 데이터(제목 등).
const homeDataEl = el("pj-home-data");
let homeTasks = {};
if (homeDataEl) {
try { homeTasks = JSON.parse(homeDataEl.textContent) || {}; } catch (_) {}
}
// ── 업무 편집 팝업 (카드/내 업무 클릭 시 페이지 이동 없이) ── // ── 업무 편집 팝업 (카드/내 업무 클릭 시 페이지 이동 없이) ──
const homeTaskModal = el("pj-modal-task"); const homeTaskModal = el("pj-modal-task");
const homeDataEl = el("pj-home-data"); if (homeTaskModal) {
if (homeTaskModal && homeDataEl) {
wireModalClose(homeTaskModal); wireModalClose(homeTaskModal);
let homeTasks = {};
try { homeTasks = JSON.parse(homeDataEl.textContent) || {}; } catch (_) {}
async function fillAssignee(pid, currentEmail) { async function fillAssignee(pid, currentEmail) {
const sel = el("pj-t-assignee"); const sel = el("pj-t-assignee");
@@ -474,6 +478,8 @@
async function openCommentsModal(taskId) { async function openCommentsModal(taskId) {
el("pj-cm-task-id").value = taskId; el("pj-cm-task-id").value = taskId;
const task = homeTasks[taskId];
el("pj-cm-title").textContent = (task && task.title) || "댓글";
openModal(commentsModal); openModal(commentsModal);
const n = await renderChat(taskId); const n = await renderChat(taskId);
if (n !== null) { markSeen(taskId, n); syncBadge(taskId, n); } if (n !== null) { markSeen(taskId, n); syncBadge(taskId, n); }