17f2d56231
- 카드에 수정(연필) 버튼: 이름·설명·기간·색상·완료 토글 + 삭제 (관리자) - 프로젝트 상태에 'completed' 추가, update_project status 허용 - 완료 프로젝트: 흐리게 비활성 + 홈 목록 맨 끝으로 정렬 - '완료 N개 숨기기' 토글 버튼(localStorage 기억) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
4.1 KiB
Python
137 lines
4.1 KiB
Python
"""프로젝트 관리 모듈 — 상수 / 기본값 / 순수 검증 로직.
|
|
|
|
DB I/O 는 db.py(ProjectStore)가 담당하고, 여기서는 상수와 입력 검증 등
|
|
DB 없이 단위 테스트 가능한 순수 함수만 둔다(malaysia.store 패턴).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
# 프로젝트 생성 시 자동으로 깔리는 기본 진행 단계(칸반 컬럼).
|
|
# (name, is_done_stage) — 마지막 '완료' 단계로 옮기면 업무 완료 처리.
|
|
DEFAULT_STAGES: tuple[tuple[str, bool], ...] = (
|
|
("할 일", False),
|
|
("진행 중", False),
|
|
("검토", False),
|
|
("완료", True),
|
|
)
|
|
|
|
# 업무 우선순위
|
|
PRIORITIES: tuple[str, ...] = ("low", "normal", "high")
|
|
PRIORITY_LABELS: dict[str, str] = {"low": "낮음", "normal": "보통", "high": "높음"}
|
|
|
|
# 프로젝트 상태
|
|
PROJECT_STATUSES: tuple[str, ...] = ("active", "completed", "archived")
|
|
|
|
# 멤버 역할
|
|
MEMBER_ROLES: tuple[str, ...] = ("manager", "member")
|
|
|
|
# 활동(activity) 종류 — 메일 트리거/타임라인 표시용
|
|
ACTION_CREATED = "created"
|
|
ACTION_ASSIGNED = "assigned"
|
|
ACTION_COMPLETED = "completed"
|
|
ACTION_STAGE_CHANGED = "stage_changed"
|
|
ACTION_REOPENED = "reopened"
|
|
|
|
# 프로젝트 색상 팔레트(아사나 느낌) — UI 선택지
|
|
COLOR_PALETTE: tuple[str, ...] = (
|
|
"#4573d2", # blue
|
|
"#37a3a3", # teal
|
|
"#62a420", # green
|
|
"#e8a33d", # amber
|
|
"#e8384f", # red
|
|
"#aa62e3", # purple
|
|
"#f06a6a", # coral
|
|
"#5a6772", # slate
|
|
)
|
|
|
|
|
|
def norm_str(s: Any, *, lower: bool = False) -> str:
|
|
out = str(s or "").strip()
|
|
return out.lower() if lower else out
|
|
|
|
|
|
def validate_priority(p: Any) -> str:
|
|
v = norm_str(p, lower=True)
|
|
return v if v in PRIORITIES else "normal"
|
|
|
|
|
|
def validate_role(r: Any) -> str:
|
|
v = norm_str(r, lower=True)
|
|
return v if v in MEMBER_ROLES else "member"
|
|
|
|
|
|
def validate_project_name(name: Any) -> str:
|
|
v = norm_str(name)
|
|
if not v:
|
|
raise ValueError("프로젝트 이름은 필수입니다.")
|
|
if len(v) > 200:
|
|
raise ValueError("프로젝트 이름이 너무 깁니다(200자 이내).")
|
|
return v
|
|
|
|
|
|
def validate_task_title(title: Any) -> str:
|
|
v = norm_str(title)
|
|
if not v:
|
|
raise ValueError("업무 제목은 필수입니다.")
|
|
if len(v) > 300:
|
|
raise ValueError("업무 제목이 너무 깁니다(300자 이내).")
|
|
return v
|
|
|
|
|
|
def validate_color(color: Any) -> str:
|
|
v = norm_str(color)
|
|
if not v:
|
|
return COLOR_PALETTE[0]
|
|
# 매우 단순한 hex 검증 — #RGB / #RRGGBB
|
|
if v.startswith("#") and len(v) in (4, 7):
|
|
try:
|
|
int(v[1:], 16)
|
|
return v.lower()
|
|
except ValueError:
|
|
pass
|
|
return COLOR_PALETTE[0]
|
|
|
|
|
|
def validate_date(d: Any) -> str | None:
|
|
"""'YYYY-MM-DD' 형식만 통과. 빈값은 None."""
|
|
from datetime import date as _date # noqa: WPS433
|
|
|
|
v = norm_str(d)
|
|
if not v:
|
|
return None
|
|
try:
|
|
_date.fromisoformat(v[:10])
|
|
except (ValueError, TypeError):
|
|
raise ValueError(f"날짜 형식이 올바르지 않습니다: {d}")
|
|
return v[:10]
|
|
|
|
|
|
def validate_time(t: Any) -> str | None:
|
|
"""'HH:MM' 형식만 통과(초는 버림). 빈값/None 은 None(=종일)."""
|
|
v = norm_str(t)
|
|
if not v:
|
|
return None
|
|
parts = v.split(":")
|
|
try:
|
|
hh = int(parts[0])
|
|
mm = int(parts[1]) if len(parts) > 1 else 0
|
|
except (ValueError, IndexError):
|
|
raise ValueError(f"시간 형식이 올바르지 않습니다: {t}")
|
|
if not (0 <= hh <= 23 and 0 <= mm <= 59):
|
|
raise ValueError(f"시간 범위가 올바르지 않습니다: {t}")
|
|
return f"{hh:02d}:{mm:02d}"
|
|
|
|
|
|
def build_subject_assigned(*, project_name: str, task_title: str, assignee: str) -> str:
|
|
return f"[DBX 프로젝트] 업무 배정: {project_name} · {task_title} → {assignee}"
|
|
|
|
|
|
def build_subject_completed(*, project_name: str, task_title: str) -> str:
|
|
return f"[DBX 프로젝트] 업무 완료: {project_name} · {task_title}"
|
|
|
|
|
|
def build_subject_project_done(*, project_name: str) -> str:
|
|
return f"[DBX 프로젝트] 프로젝트 완료: {project_name}"
|