b74d2be6d3
- 사이드 프로젝트 트리를 tasks 로부터 JS 로 재렌더(renderTree) - refreshActiveView 에서 트리도 갱신 → 새로고침 없이 좌측에 바로 표시 - 새 업무 추가 시 해당 프로젝트 자동 펼침 - projectsMeta 에 created_by 추가(트리 삭제 권한용) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
296 lines
12 KiB
HTML
296 lines
12 KiB
HTML
{% extends "erp_base.html" %}
|
||
|
||
{% block head_extra %}
|
||
<link rel="stylesheet" href="/static/project.css?v=20260626b" />
|
||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — 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" id="pj-tree-root">
|
||
{% 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="pj-cmt-emoji">💬</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 }},
|
||
"avatars": {{ avatars | tojson }}
|
||
}
|
||
</script>
|
||
|
||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||
<script src="/static/project.js?v=20260626b" defer></script>
|
||
{% endblock %}
|