fee2765905
- .pj-icon-btn 기본 스타일에 flex 중앙정렬 추가(닫기 버튼 등 호버 음영이 아이콘 기준 중앙에 오지 않던 문제, 버튼 UA 기본 패딩이 원인) - 댓글 팝업(카톡형)에서 본인이 작성한 댓글에만 삭제 버튼 노출 (서버는 이미 본인/관리자만 삭제 허용하도록 검증 중) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
335 lines
14 KiB
HTML
335 lines
14 KiB
HTML
{% extends "erp_base.html" %}
|
|
|
|
{% block head_extra %}
|
|
<link rel="stylesheet" href="/static/project.css?v=20260915h" />
|
|
<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 is_admin %}
|
|
<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>
|
|
{% if is_admin %}
|
|
<button type="button" class="pj-btn pj-btn-primary" id="pj-new-project">
|
|
+ 새 프로젝트
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if not active_projects and not done_projects %}
|
|
<div class="pj-empty">
|
|
아직 프로젝트가 없습니다.
|
|
{% if is_admin %}상단의 <b>새 프로젝트</b> 버튼으로 만들어 보세요.
|
|
{% else %}관리자가 프로젝트에 배정하면 여기에 표시됩니다.{% endif %}
|
|
</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>
|
|
|
|
<!-- 새 프로젝트 모달 -->
|
|
{% if is_admin %}
|
|
<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>
|
|
{% endif %}
|
|
|
|
<!-- 업무 편집 모달 — 카드/내 업무에서 클릭 시 페이지 이동 없이 팝업으로 편집 -->
|
|
<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" />
|
|
<input type="hidden" id="pj-t-project-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></select>
|
|
</label>
|
|
<label>단계
|
|
<select id="pj-t-stage"><option value="">불러오는 중…</option></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">
|
|
<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">
|
|
<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">삭제</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>
|
|
<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" />
|
|
<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=20260915g" defer></script>
|
|
{% endblock %}
|