Files
dbx-main/app/modules/project/templates/project/index.html
T
king 1d3f344aa3 fix(project): 보드 스크롤바가 마지막 세션 열을 가리던 문제 수정
pj-board 에 scrollbar-gutter:stable 적용 — 스크롤바 자리를 항상 미리
비워둬서, 매이슨리 열 너비를 계산하는 시점(clientWidth)과 실제 스크롤바
등장 시점이 어긋나 마지막 열이 스크롤바에 가려 보이던 문제 해결.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 22:46:40 +09:00

381 lines
17 KiB
HTML

{% extends "erp_base.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916k" />
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
{% endblock %}
{% block content %}
<div class="pj-wrap" data-is-admin="{{ 'true' if is_admin else 'false' }}"
data-is-super="{{ 'true' if user.is_super_admin else 'false' }}"
data-me="{{ user.email }}">
{% macro project_card(p) %}
<div class="pj-project-card {% if p.status == 'completed' %}is-done{% endif %}"
style="--pj-color: {{ p.color }}"
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 '' }}">
<a class="pj-project-cardlink" href="/project/p/{{ p.id }}">
<span class="pj-project-dot"></span>
<div class="pj-project-body">
<div class="pj-project-name">{{ p.name }}
{% if p.status == 'completed' %}<span class="pj-chip pj-chip-sm">완료</span>{% endif %}
</div>
{% if p.description %}<div class="pj-project-desc">{{ p.description }}</div>{% endif %}
{% if p.members %}
<div class="pj-project-members">
{% for m in p.members %}
<span class="pj-project-member">
{% if m.avatar %}<img class="pj-avatar pj-gicon-sm" src="{{ m.avatar }}" alt="" />
{% else %}<span class="material-symbols-outlined pj-gicon pj-gicon-sm">account_circle</span>{% endif %}
{{ m.name }}
</span>
{% endfor %}
</div>
{% endif %}
<div class="pj-project-meta">
{% if p.due_date %}<span class="pj-chip">마감 {{ p.due_label }}</span>{% endif %}
{% if p.task_total %}<span class="pj-chip">업무 {{ p.task_done }}/{{ p.task_total }}</span>{% endif %}
</div>
{% if p.task_total %}
<div class="pj-project-progress">
<div class="pj-project-progress-bar" style="width: {{ p.task_percent }}%"></div>
</div>
{% endif %}
</div>
</a>
<div class="pj-card-actions">
{% if p.can_add_task %}
<button type="button" class="pj-icon-btn pj-card-addtask" data-project-id="{{ p.id }}" title="업무 추가">
<span class="material-symbols-outlined">add_task</span>
</button>
{% endif %}
{% if p.can_add_task %}
<button type="button" class="pj-icon-btn pj-card-edit" title="프로젝트 수정">
<span class="material-symbols-outlined">edit</span>
</button>
{% endif %}
</div>
{% if p.task_preview %}
<ul class="pj-project-tasklist">
{% for t in p.task_preview %}
<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">
<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>
{% if t.stage_name %}<span class="pj-project-task-stage">{{ t.stage_name }}</span>{% endif %}
{% if t.assignee_display %}
<span class="pj-project-task-assignee">
{% if avatars.get(t.assignee_email | lower) %}
<img class="pj-avatar pj-gicon-sm" src="{{ avatars[t.assignee_email | lower] }}" alt="" />
{% endif %}
{{ t.assignee_display }}
</span>
{% endif %}
{% if t.due_date %}
<span class="pj-project-task-due {% if t.due_overdue %}is-overdue{% endif %}">{{ t.due_label }}</span>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
{% if p.task_more %}
<a class="pj-project-more" href="/project/p/{{ p.id }}">+{{ p.task_more }}개 업무 더보기</a>
{% endif %}
{% elif p.task_total == 0 %}
<div class="pj-project-tasks-empty">진행 중인 업무가 없습니다.</div>
{% endif %}
</div>
{% endmacro %}
<div class="pj-home">
<!-- 프로젝트 목록 -->
<section class="pj-home-main">
<div class="pj-section-head">
<h2>진행중인 프로젝트</h2>
<div class="pj-toolbar-right">
<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>
<button type="button" class="pj-btn pj-btn-primary" id="pj-new-project">
+ 새 프로젝트
</button>
</div>
</div>
{% if not active_projects and not done_projects %}
<div class="pj-empty">
아직 프로젝트가 없습니다. 상단의 <b>새 프로젝트</b> 버튼으로 만들어 보세요.
</div>
{% elif not active_projects %}
<div class="pj-empty pj-empty-sm">진행중인 프로젝트가 없습니다.</div>
{% endif %}
<div class="pj-project-grid" id="pj-project-grid">
{% for p in active_projects %}{{ project_card(p) }}{% endfor %}
</div>
{% if done_projects %}
<div class="pj-section-head pj-section-head-done">
<h2>완료된 프로젝트</h2>
<button type="button" class="pj-btn" id="pj-toggle-done" data-count="{{ done_projects | length }}">
완료 {{ done_projects | length }}개 숨기기
</button>
</div>
<div class="pj-project-grid" id="pj-project-grid-done">
{% for p in done_projects %}{{ project_card(p) }}{% endfor %}
</div>
{% endif %}
</section>
<!-- 내 업무 -->
<aside class="pj-home-side">
<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">
<a class="pj-mytree-proj" href="/project/p/{{ p.id }}">
<span class="material-symbols-outlined" style="color: {{ p.color }}; font-size:18px;">folder</span>
<span class="pj-mytree-proj-name">{{ p.name }}</span>
</a>
<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="hidden" id="pj-t-project-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></select>
</div>
<div class="pj-meta-field">
<span class="pj-meta-label">세션</span>
<select id="pj-t-stage"><option value="">불러오는 중…</option></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>
<label class="pj-upload-btn">이미지 추가<input type="file" id="pj-desc-img-input" accept="image/*" hidden /></label>
<span class="pj-muted-sm" id="pj-desc-hint"></span>
</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">
<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">
<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">
<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">
<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">삭제</button>
<span style="flex:1"></span>
<a class="pj-btn" id="pj-t-open-project" href="#" target="_self">프로젝트에서 열기</a>
<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>
<!-- 댓글만 보는 팝업 (업무 카드의 말풍선 아이콘 더블클릭) -->
<div class="pj-modal" id="pj-modal-comments" hidden>
<div class="pj-modal-card pj-modal-chat">
<div class="pj-chat-head">
<h3 id="pj-cm-title">댓글</h3>
</div>
<input type="hidden" id="pj-cm-task-id" />
<ul class="pj-chat-list" id="pj-chat-list"></ul>
<div class="pj-comment-form">
<input type="text" id="pj-cm-input" placeholder="댓글 입력…" />
<button type="button" class="pj-btn pj-btn-primary" id="pj-cm-send">전송</button>
</div>
</div>
</div>
<script id="pj-home-data" type="application/json">
{{ home_tasks | tojson }}
</script>
<script>
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
</script>
<script src="/static/project.js?v=20260916l" defer></script>
{% endblock %}