Files
dbx-main/app/modules/project/templates/project/project.html
T
king 22c4997342 feat(project): 멤버 배정을 체크박스 다중선택 + 아이콘 표시로 변경
<select> 하나씩 골라 순서대로 배정하던 걸 새 프로젝트 모달과 같은
체크박스 목록으로 바꿔 후보 전원을 한 화면에서 보고 여러 명을 한 번에
선택해 배정. 각 후보는 항상 아바타(구글 프로필, 없으면 기본 아이콘)와
이름을 같이 보여준다. 저장 시 선택된 인원마다 순차 POST 후 결과를
members 배열에 patch(기존 optimistic 흐름 그대로, reload 없음).
2026-09-16 13:03:50 +09:00

466 lines
22 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=20260917f" />
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — 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 }}"
data-name="{{ p.name }}" data-desc="{{ p.description or '' }}"
data-start="{{ p.start_date or '' }}" data-due="{{ p.due_date or '' }}"
data-color="{{ p.color }}" data-status="{{ p.status or 'active' }}"
data-creator="{{ p.created_by or '' }}">
<div class="pj-tree-row {% if p.id == project.id %}is-active{% endif %}">
<a class="pj-tree-link" href="/project/p/{{ p.id }}">
<span class="pj-tree-avatar" style="background: {{ p.color }}">{{ p.name[0] | upper }}</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-edit" title="프로젝트 수정"></button>
<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>
</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>
{% if avatars.get(m.user_email | lower) %}
<img class="pj-avatar pj-gicon-sm" src="{{ avatars[m.user_email | lower] }}" alt="" referrerpolicy="no-referrer" />
{% else %}
<span class="material-symbols-outlined pj-gicon">account_circle</span>
{% endif %}
<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="board">보드</button>
<button type="button" class="pj-tab" data-view="list">리스트</button>
<button type="button" class="pj-tab" data-view="calendar">달력</button>
<button type="button" class="pj-tab" data-view="timeline">타임라인</button>
</div>
<h1 class="pj-project-title">{{ project.name }}</h1>
<!-- 아사나처럼 기본은 "이 프로젝트"만 보여준다. 전체 프로젝트 보기는
예전처럼 접근 가능한 모든 프로젝트를 합쳐서 보여주는 모드. -->
<div class="pj-view-tabs" id="pj-scope-toggle">
<button type="button" class="pj-tab is-active" data-scope="project">이 프로젝트</button>
<button type="button" class="pj-tab" data-scope="all">전체 프로젝트</button>
</div>
</div>
<!-- 달력 (휴가 모듈과 동일한 월간 그리드) -->
<div class="pj-view" data-view="calendar" hidden>
<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 }}" {% if cell.holiday_name %}title="{{ cell.holiday_name }}"{% endif %}>
<span class="pj-day-num
{% if cell.is_sunday or cell.holiday_name %}pj-red{% elif cell.is_saturday %}pj-blue{% endif %}">{{ cell.day }}</span>
{% if cell.holiday_name %}<span class="pj-day-holiday">{{ cell.holiday_name }}</span>{% endif %}
</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>
<!-- 보드(칸반=세션) — 아사나처럼 프로젝트에 들어오면 기본으로 보이는 화면.
최대 4열 매이슨리로 배치하고, 세션이 늘어나면 pj-board 안에서만
세로 스크롤된다(좌측 프로젝트 목록·우측 내 업무와 스크롤이 서로
안 얽히게 각자 독립). "+ 세션 추가" 버튼은 스크롤 영역 밖(고정
위치)에 있어 항상 보인다. -->
<div class="pj-view" data-view="board">
<div class="pj-board-toolbar">
<button type="button" class="pj-add-stage" id="pj-board-addstage" hidden>+ 세션 추가</button>
</div>
<div class="pj-board" id="pj-board">
<div class="pj-board-canvas" id="pj-board-canvas"></div>
</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>
<!-- ── 우측: 내 업무 (홈 화면과 동일) ── -->
<aside class="pj-home-side pj-project-mytasks">
<div class="pj-section-head"><h2>내 업무</h2></div>
{% if not my_projects %}
<div class="pj-empty pj-empty-sm">배정된 업무가 없습니다.</div>
{% else %}
<div class="pj-mytree">
{% for p in my_projects %}
<div class="pj-mytree-group" data-group-id="{{ p.id }}">
<div class="pj-mytree-head">
<button type="button" class="pj-mytree-toggle" title="펼치기/접기">
<span class="material-symbols-outlined" style="color: {{ p.color }}; font-size:18px;">folder_open</span>
</button>
<a class="pj-mytree-proj" href="/project/p/{{ p.id }}">
<span class="pj-mytree-proj-name">{{ p.name }}</span>
</a>
</div>
<ul class="pj-mytree-tasks">
{% for t in p.tasks %}
<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">
<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-title">{{ t.title }}</span>
{% if t.start_date or t.due_date %}
<span class="pj-mytree-dates">
{% if t.start_date and t.due_date %}{{ t.start_label }} ~ {{ t.due_label }}
{% elif t.due_date %}~ {{ t.due_label }}
{% else %}{{ t.start_label }} ~{% endif %}
</span>
{% endif %}
</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endif %}
</aside>
</div>
</div>
<!-- 새 프로젝트 모달 — 좌측 프로젝트 목록의 + 버튼(홈 화면과 동일 폼) -->
<div class="pj-modal" id="pj-modal-project" hidden>
<div class="pj-modal-card">
<h3>새 프로젝트</h3>
<label>이름<input type="text" id="pj-f-name" placeholder="프로젝트 이름" /></label>
<label>설명<textarea id="pj-f-desc" rows="2"></textarea></label>
<div class="pj-form-row">
<label>시작일<input type="date" id="pj-f-start" /></label>
<label>마감일<input type="date" id="pj-f-due" /></label>
</div>
<label>색상
<span class="pj-color-palette" id="pj-f-colors"></span>
</label>
<div class="pj-field-label">멤버
<div class="pj-member-picker" id="pj-f-members">불러오는 중…</div>
</div>
<div class="pj-modal-actions">
<button type="button" class="pj-btn" data-close>취소</button>
<button type="button" class="pj-btn pj-btn-primary" id="pj-save-project">만들기</button>
</div>
</div>
</div>
<!-- 프로젝트 수정 모달 — 좌측 프로젝트 목록의 ✎ 버튼 -->
<div class="pj-modal" id="pj-modal-edit" hidden>
<div class="pj-modal-card">
<h3>프로젝트 수정</h3>
<input type="hidden" id="pj-e-id" />
<label>이름<input type="text" id="pj-e-name" placeholder="프로젝트 이름" /></label>
<label>설명<textarea id="pj-e-desc" rows="2"></textarea></label>
<div class="pj-form-row">
<label>시작일<input type="date" id="pj-e-start" /></label>
<label>마감일<input type="date" id="pj-e-due" /></label>
</div>
<label>색상
<span class="pj-color-palette" id="pj-e-colors"></span>
</label>
<label class="pj-time-toggle">
<input type="checkbox" id="pj-e-done" /> 프로젝트 완료 (목록 끝으로 이동·비활성)
</label>
<div class="pj-modal-actions">
<button type="button" class="pj-btn pj-btn-danger" id="pj-delete-project">삭제</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-edit">저장</button>
</div>
</div>
</div>
<!-- 업무 모달 -->
<div class="pj-modal" id="pj-modal-task" hidden>
<div class="pj-modal-card pj-task-modal-card">
<!-- 아사나처럼 큰 제목은 아래 무테 입력칸이 대신한다 — 이 표시는 작은
모드 안내(새 업무/업무 편집)만 한다. -->
<h3 id="pj-task-modal-title" class="pj-modal-kicker">새 업무</h3>
<input type="hidden" id="pj-t-id" />
<input type="text" id="pj-t-title" class="pj-t-title-input" placeholder="업무 제목" />
<div class="pj-modal-body">
<div class="pj-modal-col-main">
<!-- 메타 정보 — 담당자·세션·우선순위·일정을 작은 칩 형태로 한 줄에 -->
<div class="pj-meta-row">
<div class="pj-meta-field">
<span class="pj-meta-label">담당자</span>
<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>
</div>
<div class="pj-meta-field">
<span class="pj-meta-label">세션</span>
<select id="pj-t-stage">
{% for s in stages %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="pj-meta-field">
<span class="pj-meta-label">우선순위</span>
<select id="pj-t-priority">
<option value="low">낮음</option>
<option value="normal" selected>보통</option>
<option value="high">높음</option>
</select>
</div>
</div>
<div class="pj-meta-row">
<div class="pj-meta-field">
<span class="pj-meta-label">시작일</span>
<input type="date" id="pj-t-start" />
</div>
<div class="pj-meta-field">
<span class="pj-meta-label">마감일</span>
<input type="date" id="pj-t-due" />
</div>
</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-desc-toolbar">
<span class="pj-extra-label">설명</span>
<span class="pj-muted-sm" id="pj-desc-hint"></span>
</div>
<div class="pj-desc-fmtbar" id="pj-desc-fmtbar">
<button type="button" class="pj-desc-fmtbtn" data-cmd="bold" title="굵게 (Ctrl+B)"><b>B</b></button>
<button type="button" class="pj-desc-fmtbtn" data-cmd="italic" title="기울임 (Ctrl+I)"><i>I</i></button>
<button type="button" class="pj-desc-fmtbtn" data-cmd="underline" title="밑줄 (Ctrl+U)"><u>U</u></button>
<span class="pj-desc-fmtsep"></span>
<button type="button" class="pj-desc-fmtbtn" data-cmd="insertUnorderedList" title="글머리 기호 목록">☰•</button>
<button type="button" class="pj-desc-fmtbtn" data-cmd="insertOrderedList" title="번호 매기기 목록">☰1</button>
<button type="button" class="pj-desc-fmtbtn" data-cmd="formatBlock" data-arg="blockquote" title="인용구"></button>
<span class="pj-desc-fmtsep"></span>
<button type="button" class="pj-desc-fmtbtn" data-cmd="createLink" title="링크 삽입">🔗</button>
<button type="button" class="pj-desc-fmtbtn" data-cmd="removeFormat" title="서식 지우기">✕서식</button>
<span class="pj-desc-fmtsep"></span>
<label class="pj-desc-fmtbtn" id="pj-desc-imgbtn" title="이미지 추가"><span class="material-symbols-outlined">image</span><input type="file" id="pj-desc-img-input" accept="image/*" hidden /></label>
</div>
<div class="pj-desc-wrap">
<div class="pj-desc-editor" id="pj-t-desc" contenteditable="true" data-placeholder="설명을 입력하세요… (이미지는 붙여넣기 또는 위 버튼으로 추가)"></div>
</div>
</div>
<div class="pj-modal-col-side">
<!-- 하위 업무 (기존 업무 편집 시에만) -->
<div class="pj-task-extra" id="pj-subtask-block" hidden>
<div class="pj-extra-head">
<span class="material-symbols-outlined pj-gicon pj-gicon-sm">checklist</span> 하위 업무
</div>
<ul class="pj-subtask-list" id="pj-subtask-list"></ul>
<div class="pj-subtask-form">
<input type="text" id="pj-subtask-input" placeholder="하위 업무 제목…" />
<select id="pj-subtask-add-assignee"><option value="">(미지정)</option></select>
<input type="date" id="pj-subtask-add-due" />
<button type="button" class="pj-btn pj-btn-primary" id="pj-subtask-add">추가</button>
</div>
</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-links-block" hidden>
<div class="pj-extra-head">
<span class="material-symbols-outlined pj-gicon pj-gicon-sm">link</span> 연결된 프로젝트
</div>
<div class="pj-link-chips" id="pj-link-chips"></div>
<div class="pj-link-form" id="pj-link-form">
<select id="pj-link-project"><option value="">프로젝트 선택…</option></select>
<select id="pj-link-stage"><option value="">세션 선택…</option></select>
<button type="button" class="pj-btn pj-btn-primary" id="pj-link-save">추가</button>
</div>
</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>
</div>
<div class="pj-modal-actions">
<button type="button" class="pj-btn pj-btn-complete" id="pj-toggle-complete" hidden>✓ 완료로 표시</button>
<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>
<div class="pj-field-label">직원
<div class="pj-member-picker" id="pj-m-users">불러오는 중…</div>
</div>
<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>window.PJ_COLORS = {{ color_palette | tojson }};</script>
<!-- 데이터 -->
<script id="pj-data" type="application/json">
{
"project": {{ project | tojson }},
"currentProjectId": {{ current_project_id | 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 }},
"myTasks": {{ my_tasks | tojson }}
}
</script>
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
<script src="/static/project.js?v=20260917g" defer></script>
{% endblock %}