feat(project): 아사나식 프로젝트 관리 모듈 추가
- project_db 신규(projects/members/stages/tasks/comments/activity, project_app CRUD) - 프로젝트/서브프로젝트(self-FK), 업무·진행단계(칸반), 멤버 배정 - 메인 뷰 달력(FullCalendar)/타임라인(vis-timeline) 토글 + 보드/리스트 - 진입은 로그인 회사 직원 전원, 생성/배정은 관리자 전용 - 업무 배정·완료 시 관리자 메일 알림(app/mail.py, SMTP_* env, 미설정 시 skip) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260625a" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pj-wrap" data-is-admin="{{ 'true' if is_admin else 'false' }}">
|
||||
|
||||
<div class="pj-home">
|
||||
<!-- 프로젝트 목록 -->
|
||||
<section class="pj-home-main">
|
||||
<div class="pj-section-head">
|
||||
<h2>프로젝트</h2>
|
||||
{% if is_admin %}
|
||||
<button type="button" class="pj-btn pj-btn-primary" id="pj-new-project">
|
||||
+ 새 프로젝트
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not tree %}
|
||||
<div class="pj-empty">
|
||||
아직 프로젝트가 없습니다.
|
||||
{% if is_admin %}상단의 <b>새 프로젝트</b> 버튼으로 만들어 보세요.
|
||||
{% else %}관리자가 프로젝트에 배정하면 여기에 표시됩니다.{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="pj-project-grid">
|
||||
{% for p in tree %}
|
||||
<a class="pj-project-card" href="/project/p/{{ p.id }}"
|
||||
style="--pj-color: {{ p.color }}">
|
||||
<span class="pj-project-dot"></span>
|
||||
<div class="pj-project-body">
|
||||
<div class="pj-project-name">{{ p.name }}</div>
|
||||
{% if p.description %}<div class="pj-project-desc">{{ p.description }}</div>{% endif %}
|
||||
<div class="pj-project-meta">
|
||||
{% if p.children %}<span class="pj-chip">서브 {{ p.children|length }}</span>{% endif %}
|
||||
{% if p.due_date %}<span class="pj-chip">마감 {{ p.due_date }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 내 업무 -->
|
||||
<aside class="pj-home-side">
|
||||
<div class="pj-section-head"><h2>내 업무</h2></div>
|
||||
{% if not my_tasks %}
|
||||
<div class="pj-empty pj-empty-sm">배정된 업무가 없습니다.</div>
|
||||
{% else %}
|
||||
<ul class="pj-mytask-list">
|
||||
{% for t in my_tasks %}
|
||||
<li class="pj-mytask {% if t.completed_at %}is-done{% endif %}">
|
||||
<a href="/project/p/{{ t.project_id }}">
|
||||
<span class="pj-mytask-dot" style="background: {{ t.project_color }}"></span>
|
||||
<span class="pj-mytask-title">{{ t.title }}</span>
|
||||
<span class="pj-mytask-meta">
|
||||
{{ t.project_name }}{% if t.due_date %} · {{ t.due_date }}{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% 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-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>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||
</script>
|
||||
<script src="/static/project.js?v=20260625a" defer></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,166 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260625a" />
|
||||
<!-- FullCalendar (달력) · vis-timeline (타임라인) — 스켈레톤 단계는 CDN, 추후 self-host -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.3/styles/vis-timeline-graph2d.min.css" rel="stylesheet" />
|
||||
{% 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-me="{{ user.email }}">
|
||||
|
||||
<div class="pj-layout">
|
||||
<!-- ── 좌측: 서브프로젝트 + 멤버 ── -->
|
||||
<aside class="pj-side">
|
||||
<div class="pj-side-block">
|
||||
<div class="pj-side-head">
|
||||
<span>서브프로젝트</span>
|
||||
{% if can_manage %}
|
||||
<button type="button" class="pj-icon-btn" id="pj-new-sub" title="서브프로젝트 추가">+</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<ul class="pj-sub-list" id="pj-sub-list">
|
||||
{% for s in subprojects %}
|
||||
<li>
|
||||
<a href="/project/p/{{ s.id }}">
|
||||
<span class="pj-sub-dot" style="background: {{ s.color }}"></span>{{ s.name }}
|
||||
</a>
|
||||
{% if can_manage %}
|
||||
<button type="button" class="pj-icon-btn pj-del-sub" data-id="{{ s.id }}" title="삭제">×</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="pj-empty-sm">없음</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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="pj-avatar">{{ (m.user_name or m.user_email)[0] | upper }}</span>
|
||||
<span class="pj-member-name">{{ m.user_name or m.user_email }}</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>
|
||||
{% if can_manage %}
|
||||
<button type="button" class="pj-btn pj-btn-primary" id="pj-new-task">+ 업무 추가</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 달력 -->
|
||||
<div class="pj-view" data-view="calendar">
|
||||
<div id="pj-calendar"></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_name or m.user_email }}">
|
||||
{{ m.user_name or m.user_email }}
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<!-- 데이터 -->
|
||||
<script id="pj-data" type="application/json">
|
||||
{
|
||||
"project": {{ project | tojson }},
|
||||
"stages": {{ stages | tojson }},
|
||||
"tasks": {{ tasks | tojson }},
|
||||
"members": {{ members | tojson }},
|
||||
"priorityLabels": {{ priority_labels | tojson }}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.3/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
||||
<script src="/static/project.js?v=20260625a" defer></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user