Files
dbx-main/app/modules/project/templates/project/project.html
T
king e4fb945474 feat(project): 댓글 있는 업무에 말풍선 아이콘 표시(달력·타임라인·보드·리스트)
- list_tasks/get_task 에 comment_count 서브쿼리 추가
- 달력 막대/타임라인 항목/보드 카드/리스트 업무열에 댓글 수 말풍선
- 댓글 로드 시 메모리 카운트 동기화

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:17:48 +09:00

295 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "erp_base.html" %}
{% block head_extra %}
<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 -->
<link href="/static/vendor/vis-timeline/vis-timeline-graph2d.min.css" rel="stylesheet" />
{% endblock %}
{% block topbar_actions %}
<a class="pj-bell" href="/project/inbox" title="알림센터">
<span class="material-symbols-outlined">notifications</span>
<span class="pj-bell-badge" id="pj-bell-badge" hidden>0</span>
</a>
{% if can_manage %}
<button type="button" class="pj-btn pj-btn-primary" id="pj-new-task">+ 업무 추가</button>
{% endif %}
{% endblock %}
{% block content %}
<div class="pj-wrap pj-project-view"
data-project-id="{{ project.id }}"
data-can-manage="{{ 'true' if can_manage else 'false' }}"
data-is-admin="{{ 'true' if is_admin else 'false' }}"
data-is-super="{{ 'true' if user.is_super_admin else 'false' }}"
data-me="{{ user.email }}">
<div class="pj-layout">
<!-- ── 좌측: 프로젝트 트리 + 멤버 ── -->
<aside class="pj-side">
<div class="pj-side-block">
<div class="pj-side-head">
<span>프로젝트</span>
{% if is_admin %}
<button type="button" class="pj-icon-btn" id="pj-new-project-side" title="새 프로젝트">+</button>
{% endif %}
</div>
{% if nav_projects %}
<ul class="pj-tree">
{% for p in nav_projects %}
<li class="pj-tree-li" data-id="{{ p.id }}">
<div class="pj-tree-row {% if p.id == project.id %}is-active{% endif %}">
{% if p.tasks %}
<button type="button" class="pj-tree-caret" aria-expanded="false" title="펼치기/접기">
<span class="material-symbols-outlined">chevron_right</span>
</button>
{% else %}
<span class="pj-tree-caret-spacer"></span>
{% endif %}
<a class="pj-tree-link" href="/project/p/{{ p.id }}">
<span class="pj-tree-icon material-symbols-outlined" style="color: {{ p.color }}">folder</span>
<span class="pj-tree-name">{{ p.name }}</span>
</a>
{% if is_admin %}
<span class="pj-tree-acts">
<button type="button" class="pj-icon-btn pj-tree-del" data-id="{{ p.id }}" data-name="{{ p.name }}" data-creator="{{ p.created_by }}" title="프로젝트 삭제">×</button>
</span>
{% endif %}
</div>
{% if p.tasks %}
<div class="pj-tree-children" hidden>
<ul class="pj-tree">
{% for t in p.tasks %}
<li class="pj-tree-li pj-tree-task">
<div class="pj-tree-row" style="--pj-indent: 1">
<span class="pj-tree-caret-spacer"></span>
<a class="pj-tree-link pj-tree-tasklink"
href="/project/p/{{ p.id }}?task={{ t.id }}"
data-project="{{ p.id }}" data-task="{{ t.id }}">
<span class="pj-tree-icon material-symbols-outlined" style="color: {{ t.color }}">{% if t.completed %}task_alt{% else %}radio_button_unchecked{% endif %}</span>
<span class="pj-tree-name {% if t.completed %}pj-tree-done{% endif %}">{{ t.title }}</span>
</a>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div class="pj-empty-sm">프로젝트 없음</div>
{% endif %}
</div>
<div class="pj-side-block">
<div class="pj-side-head">
<span>멤버</span>
{% if is_admin %}
<button type="button" class="pj-icon-btn" id="pj-add-member" title="멤버 배정">+</button>
{% endif %}
</div>
<ul class="pj-member-list" id="pj-member-list">
{% for m in members %}
<li>
<span class="material-symbols-outlined pj-gicon">account_circle</span>
<span class="pj-member-name">{{ m.user_email.split('@')[0] }}</span>
{% if m.role == 'manager' %}<span class="pj-chip pj-chip-sm">관리</span>{% endif %}
{% if is_admin %}
<button type="button" class="pj-icon-btn pj-del-member" data-email="{{ m.user_email }}" title="제외">×</button>
{% endif %}
</li>
{% else %}
<li class="pj-empty-sm">없음</li>
{% endfor %}
</ul>
</div>
</aside>
<!-- ── 우측: 뷰 ── -->
<section class="pj-main">
<div class="pj-toolbar">
<div class="pj-view-tabs" id="pj-view-tabs">
<button type="button" class="pj-tab is-active" data-view="calendar">달력</button>
<button type="button" class="pj-tab" data-view="timeline">타임라인</button>
<button type="button" class="pj-tab" data-view="board">보드</button>
<button type="button" class="pj-tab" data-view="list">리스트</button>
</div>
</div>
<!-- 달력 (휴가 모듈과 동일한 월간 그리드) -->
<div class="pj-view" data-view="calendar">
<div class="pj-cal-head">
<a class="pj-btn pj-nav-btn" href="/project/p/{{ project.id }}?y={{ prev_y }}&m={{ prev_m }}"></a>
<h2 class="pj-cal-title">{{ year }}년 {{ month }}월</h2>
<a class="pj-btn pj-nav-btn" href="/project/p/{{ project.id }}?y={{ next_y }}&m={{ next_m }}"></a>
<a class="pj-btn pj-today-btn" href="/project/p/{{ project.id }}">오늘</a>
</div>
<div class="pj-cal">
<div class="pj-wd-row">
{% for wd in weekdays %}
<div class="pj-wd {% if loop.index0 == 0 %}pj-red{% elif loop.index0 == 6 %}pj-blue{% endif %}">{{ wd }}</div>
{% endfor %}
</div>
{% for week in weeks %}
<div class="pj-week">
<div class="pj-week-days">
{% for cell in week.days %}
<div class="pj-day {% if not cell.in_month %}pj-out{% endif %} {% if cell.is_today %}pj-today{% endif %}"
data-date="{{ cell.date }}">
<span class="pj-day-num
{% if cell.is_sunday %}pj-red{% elif cell.is_saturday %}pj-blue{% endif %}">{{ cell.day }}</span>
</div>
{% endfor %}
</div>
<!-- 막대는 클라이언트(JS)가 그린다 — 깔끔한 칩 + 행 제한(+N) + 드래그 이동 -->
<div class="pj-week-bars"></div>
</div>
{% endfor %}
</div>
</div>
<!-- 타임라인 -->
<div class="pj-view" data-view="timeline" hidden>
<div id="pj-timeline"></div>
</div>
<!-- 보드(칸반) -->
<div class="pj-view" data-view="board" hidden>
<div class="pj-board" id="pj-board"></div>
</div>
<!-- 리스트 -->
<div class="pj-view" data-view="list" hidden>
<table class="pj-table" id="pj-list">
<thead>
<tr><th>업무</th><th>담당자</th><th>단계</th><th>우선순위</th><th>시작</th><th>마감</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
</div>
</div>
<!-- 업무 모달 -->
<div class="pj-modal" id="pj-modal-task" hidden>
<div class="pj-modal-card">
<h3 id="pj-task-modal-title">새 업무</h3>
<input type="hidden" id="pj-t-id" />
<label>제목<input type="text" id="pj-t-title" placeholder="업무 제목" /></label>
<label>설명<textarea id="pj-t-desc" rows="2"></textarea></label>
<div class="pj-form-row">
<label>담당자
<select id="pj-t-assignee">
<option value="">(미지정)</option>
{% for m in members %}
<option value="{{ m.user_email }}" data-name="{{ m.user_email.split('@')[0] }}">
{{ m.user_email.split('@')[0] }}
</option>
{% endfor %}
</select>
</label>
<label>단계
<select id="pj-t-stage">
{% for s in stages %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</label>
</div>
<div class="pj-form-row">
<label>우선순위
<select id="pj-t-priority">
<option value="low">낮음</option>
<option value="normal" selected>보통</option>
<option value="high">높음</option>
</select>
</label>
<label>시작일<input type="date" id="pj-t-start" /></label>
<label>마감일<input type="date" id="pj-t-due" /></label>
</div>
<label class="pj-time-toggle">
<input type="checkbox" id="pj-t-usetime" /> 시간 지정 (체크 안 하면 종일)
</label>
<div class="pj-form-row" id="pj-time-row">
<label>시작 시간<input type="time" id="pj-t-stime" disabled /></label>
<label>마감 시간<input type="time" id="pj-t-dtime" disabled /></label>
</div>
<!-- 첨부파일 (기존 업무 편집 시에만) -->
<div class="pj-task-extra" id="pj-attach-block" hidden>
<div class="pj-extra-head">
<span class="material-symbols-outlined pj-gicon pj-gicon-sm">attach_file</span> 첨부파일
<label class="pj-upload-btn">
파일 추가<input type="file" id="pj-attach-input" hidden />
</label>
</div>
<ul class="pj-attach-list" id="pj-attach-list"></ul>
</div>
<!-- 댓글 (기존 업무 편집 시에만) -->
<div class="pj-task-extra" id="pj-comment-block" hidden>
<div class="pj-extra-head">
<span class="material-symbols-outlined pj-gicon pj-gicon-sm">chat_bubble</span> 댓글
</div>
<ul class="pj-comment-list" id="pj-comment-list"></ul>
<div class="pj-comment-form">
<input type="text" id="pj-comment-input" placeholder="댓글 입력…" />
<button type="button" class="pj-btn pj-btn-primary" id="pj-comment-send">등록</button>
</div>
</div>
<div class="pj-modal-actions">
<button type="button" class="pj-btn pj-btn-danger" id="pj-delete-task" hidden>삭제</button>
<span style="flex:1"></span>
<button type="button" class="pj-btn" data-close>취소</button>
<button type="button" class="pj-btn pj-btn-primary" id="pj-save-task">저장</button>
</div>
</div>
</div>
{% if is_admin %}
<!-- 멤버 배정 모달 — 등록된 사용자(프로젝트 권한 보유)를 자동 목록화 -->
<div class="pj-modal" id="pj-modal-member" hidden>
<div class="pj-modal-card">
<h3>멤버 배정</h3>
<p class="pj-modal-hint">관리자 페이지에서 <b>프로젝트 관리</b> 권한을 켠 직원이 후보로 나옵니다.</p>
<label>직원
<select id="pj-m-user"><option value="">불러오는 중…</option></select>
</label>
<label>역할
<select id="pj-m-role">
<option value="member" selected>멤버</option>
<option value="manager">관리(서브·업무 관리)</option>
</select>
</label>
<div class="pj-modal-actions">
<span style="flex:1"></span>
<button type="button" class="pj-btn" data-close>취소</button>
<button type="button" class="pj-btn pj-btn-primary" id="pj-save-member">배정</button>
</div>
</div>
</div>
{% endif %}
<!-- 데이터 -->
<script id="pj-data" type="application/json">
{
"project": {{ project | tojson }},
"stages": {{ stages | tojson }},
"tasks": {{ tasks | tojson }},
"members": {{ members | tojson }},
"priorityLabels": {{ priority_labels | tojson }},
"projects": {{ projects_meta | tojson }},
"stagesByProject": {{ stages_by_project | tojson }},
"boardStages": {{ board_stage_names | tojson }}
}
</script>
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
<script src="/static/project.js?v=20260625x" defer></script>
{% endblock %}