feat(project): 세션 색상/편집, 프로젝트 트리 드래그, 멤버업무 정렬, 댓글팝업 버그수정
- 좌측 프로젝트 리스트 드래그 재정렬(관리자, 홈 카드와 동일 API) - 세션 추가/편집 팝업: 이름 + 아사나 10색 색상표(project_stages.color, 마이그레이션 004) - 보드 세션 hover 아이콘 확대, 세션 제목 강조(업무 카드보다 크고 굵게) - 완료 세션 반투명 + 보드 끝으로(정렬 기준만 변경 — 해제 시 원래 순서 자동 복귀) - 전체 멤버 업무 팝업에 표 헤더(가운데정렬/굵게) + 정렬 아이콘 + 더블클릭 편집 - 댓글 채팅 팝업을 initCommentsChat() 공용화 — project.html에 모달이 없어 "내 업무" 패널 댓글 더블클릭이 전혀 동작 안 했던 버그 수정(동적 배지 재바인딩 누락도 같이 수정) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -238,11 +238,13 @@ class ProjectStore:
|
||||
return [self._serialize(r) for r in rows]
|
||||
|
||||
def add_stage(
|
||||
self, *, project_id: int, name: str, is_done_stage: bool = False, created_by: str = ""
|
||||
self, *, project_id: int, name: str, is_done_stage: bool = False,
|
||||
color: str | None = None, created_by: str = "",
|
||||
) -> dict[str, Any]:
|
||||
nm = store.norm_str(name)
|
||||
if not nm:
|
||||
raise ValueError("세션 이름은 필수입니다.")
|
||||
col = store.validate_stage_color(color)
|
||||
with self._pool.connection() as conn:
|
||||
nxt = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order), -1) + 1 AS n "
|
||||
@@ -250,9 +252,9 @@ class ProjectStore:
|
||||
(project_id,),
|
||||
).fetchone()
|
||||
row = conn.execute(
|
||||
"INSERT INTO project_stages (project_id, name, sort_order, is_done_stage, created_by) "
|
||||
"VALUES (%s,%s,%s,%s,%s) RETURNING *",
|
||||
(project_id, nm, int(nxt["n"]), bool(is_done_stage),
|
||||
"INSERT INTO project_stages (project_id, name, sort_order, is_done_stage, color, created_by) "
|
||||
"VALUES (%s,%s,%s,%s,%s,%s) RETURNING *",
|
||||
(project_id, nm, int(nxt["n"]), bool(is_done_stage), col,
|
||||
store.norm_str(created_by, lower=True)),
|
||||
).fetchone()
|
||||
return self._serialize(row)
|
||||
@@ -776,8 +778,10 @@ class ProjectStore:
|
||||
# ════════════════════════════════════════════════════════════
|
||||
def update_stage(
|
||||
self, *, stage_id: int, name: str | None = None,
|
||||
is_done_stage: bool | None = None,
|
||||
is_done_stage: bool | None = None, color: Any = ...,
|
||||
) -> dict[str, Any]:
|
||||
"""color 는 지정 안 함(기본 `...`)과 명시적으로 지우기(`None`)를
|
||||
구분해야 해서 다른 필드처럼 `is None` 이 아니라 `is not ...` 로 검사한다."""
|
||||
sets: list[str] = []
|
||||
params: list[Any] = []
|
||||
if name is not None:
|
||||
@@ -789,6 +793,9 @@ class ProjectStore:
|
||||
if is_done_stage is not None:
|
||||
sets.append("is_done_stage = %s")
|
||||
params.append(bool(is_done_stage))
|
||||
if color is not ...:
|
||||
sets.append("color = %s")
|
||||
params.append(store.validate_stage_color(color))
|
||||
if not sets:
|
||||
raise ValueError("변경할 내용이 없습니다.")
|
||||
params.append(stage_id)
|
||||
|
||||
@@ -618,6 +618,7 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse:
|
||||
"avatars": avatars,
|
||||
"priority_labels": store.PRIORITY_LABELS,
|
||||
"color_palette": list(store.COLOR_PALETTE),
|
||||
"stage_color_palette": list(store.STAGE_COLOR_PALETTE),
|
||||
"today": today.isoformat(),
|
||||
"year": year,
|
||||
"month": month,
|
||||
@@ -865,6 +866,7 @@ async def api_add_stage(
|
||||
project_id=project_id,
|
||||
name=payload.get("name", ""),
|
||||
is_done_stage=bool(payload.get("is_done_stage", False)),
|
||||
color=payload.get("color"),
|
||||
created_by=user["email"],
|
||||
)
|
||||
except ValueError as exc:
|
||||
@@ -1362,6 +1364,7 @@ async def api_update_stage(
|
||||
stage_id=stage_id,
|
||||
name=payload.get("name"),
|
||||
is_done_stage=payload.get("is_done_stage"),
|
||||
color=payload.get("color", ...),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -49,6 +49,21 @@ COLOR_PALETTE: tuple[str, ...] = (
|
||||
"#5a6772", # slate
|
||||
)
|
||||
|
||||
# 세션(칸반 컬럼) 색상 팔레트 — 아사나 태그/커스텀필드에서 쓰는 10색.
|
||||
# 세션 생성/편집 팝업의 색상표가 이 순서 그대로 노출된다.
|
||||
STAGE_COLOR_PALETTE: tuple[str, ...] = (
|
||||
"#4573d2", # blue
|
||||
"#37a3a3", # teal
|
||||
"#62a420", # green
|
||||
"#e8a33d", # amber
|
||||
"#e8384f", # red
|
||||
"#aa62e3", # purple
|
||||
"#f06a6a", # coral
|
||||
"#5a6772", # slate
|
||||
"#e8398a", # pink
|
||||
"#a15c43", # brown
|
||||
)
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 대한민국 국경일·공휴일(2026~2030) — 프로젝트 달력 표시 전용.
|
||||
#
|
||||
@@ -181,6 +196,12 @@ def validate_color(color: Any) -> str:
|
||||
return COLOR_PALETTE[0]
|
||||
|
||||
|
||||
def validate_stage_color(color: Any) -> str | None:
|
||||
"""세션 색상 — 팔레트 밖 값/빈값은 저장 안 함(NULL, 기본 회색 표시)."""
|
||||
v = norm_str(color).lower()
|
||||
return v if v in STAGE_COLOR_PALETTE else None
|
||||
|
||||
|
||||
def validate_date(d: Any) -> str | None:
|
||||
"""'YYYY-MM-DD' 형식만 통과. 빈값은 None."""
|
||||
from datetime import date as _date # noqa: WPS433
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917j" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917k" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="/static/project.js?v=20260917o" defer></script>
|
||||
<script src="/static/project.js?v=20260917p" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917j" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917k" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -393,5 +393,5 @@
|
||||
<script>
|
||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||
</script>
|
||||
<script src="/static/project.js?v=20260917o" defer></script>
|
||||
<script src="/static/project.js?v=20260917p" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917j" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917k" />
|
||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
<!-- 타임라인 vis-timeline — self-host -->
|
||||
@@ -290,6 +290,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 세션 추가/편집 모달 — 보드의 "+ 세션 추가" 버튼과 세션 헤더의 ✎(세션 편집) 공용 -->
|
||||
<div class="pj-modal" id="pj-modal-stage" hidden>
|
||||
<div class="pj-modal-card">
|
||||
<h3 id="pj-stage-modal-title">새 세션</h3>
|
||||
<input type="hidden" id="pj-stg-id" />
|
||||
<label>이름<input type="text" id="pj-stg-name" placeholder="세션 이름" /></label>
|
||||
<label>색상
|
||||
<span class="pj-color-palette" id="pj-stg-colors"></span>
|
||||
</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-stage">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 업무 모달 -->
|
||||
<div class="pj-modal" id="pj-modal-task" hidden>
|
||||
<div class="pj-modal-card pj-task-modal-card">
|
||||
@@ -460,10 +477,32 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 댓글만 보는 채팅형 팝업 — 업무 카드/내 업무의 말풍선 아이콘 더블클릭(홈 화면과 동일 기능) -->
|
||||
<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>
|
||||
|
||||
<!-- 전체 멤버 아이콘 클릭 팝업 — 그 사람이 담당자인 업무를 프로젝트/세션/마감일과 함께 나열 -->
|
||||
<div class="pj-modal" id="pj-modal-member-tasks" hidden>
|
||||
<div class="pj-modal-card pj-mt-card">
|
||||
<h3 id="pj-mt-title">업무</h3>
|
||||
<p class="pj-modal-hint">더블클릭하면 업무 편집 창이 열립니다.</p>
|
||||
<div class="pj-mt-head" id="pj-mt-head">
|
||||
<span class="pj-mt-h" data-sort="project">프로젝트<span class="pj-mt-sort-icon"></span></span>
|
||||
<span class="pj-mt-h" data-sort="stage">세션<span class="pj-mt-sort-icon"></span></span>
|
||||
<span class="pj-mt-h" data-sort="title">업무<span class="pj-mt-sort-icon"></span></span>
|
||||
<span class="pj-mt-h" data-sort="due">마감일<span class="pj-mt-sort-icon"></span></span>
|
||||
</div>
|
||||
<ul class="pj-mt-list" id="pj-mt-list"></ul>
|
||||
<div class="pj-modal-actions">
|
||||
<span style="flex:1"></span>
|
||||
@@ -472,7 +511,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>window.PJ_COLORS = {{ color_palette | tojson }};</script>
|
||||
<script>
|
||||
window.PJ_COLORS = {{ color_palette | tojson }};
|
||||
window.PJ_STAGE_COLORS = {{ stage_color_palette | tojson }};
|
||||
</script>
|
||||
<!-- 데이터 -->
|
||||
<script id="pj-data" type="application/json">
|
||||
{
|
||||
@@ -491,5 +533,5 @@
|
||||
</script>
|
||||
|
||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||
<script src="/static/project.js?v=20260917o" defer></script>
|
||||
<script src="/static/project.js?v=20260917p" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user