feat(project): 업무 시간 지정(종일 기본) + 타임라인 시간 반영

- tasks 에 start_time/due_time TIME 컬럼 추가(마이그레이션 003). NULL=종일
- 업무 모달에 '시간 지정' 체크박스 → 체크 시 시작/마감 시간 입력란 표시.
  미체크면 종일로 저장(시간 null)
- store.validate_time(HH:MM), db create/update + TIME 직렬화('HH:MM')
- 타임라인: 시간 있으면 date+time 으로 정확히 배치, 없으면 종일

DB: scripts/sql/project_db_003_task_times.sql 서버서 적용 필요.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 13:34:52 +09:00
parent a6adb5fc41
commit 133bfc55b8
9 changed files with 90 additions and 13 deletions
+17 -4
View File
@@ -13,7 +13,7 @@
from __future__ import annotations
import logging
from datetime import date, datetime
from datetime import date, datetime, time
from typing import Any
from psycopg.rows import dict_row
@@ -331,12 +331,16 @@ class ProjectStore:
priority: str = "normal",
start_date: str | None = None,
due_date: str | None = None,
start_time: str | None = None,
due_time: str | None = None,
created_by: str = "",
) -> dict[str, Any]:
ttl = store.validate_task_title(title)
pr = store.validate_priority(priority)
sd = store.validate_date(start_date)
dd = store.validate_date(due_date)
sti = store.validate_time(start_time)
dti = store.validate_time(due_time)
assignee = store.norm_str(assignee_email, lower=True)
with self._pool.connection() as conn:
with conn.transaction():
@@ -357,14 +361,15 @@ class ProjectStore:
"""
INSERT INTO tasks
(project_id, stage_id, title, description, assignee_email,
assignee_name, priority, start_date, due_date, sort_order, created_by)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
assignee_name, priority, start_date, due_date,
start_time, due_time, sort_order, created_by)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING *
""",
(
project_id, stage_id, ttl, store.norm_str(description),
assignee, store.norm_str(assignee_name), pr, sd, dd,
int(nxt["n"]), store.norm_str(created_by, lower=True),
sti, dti, int(nxt["n"]), store.norm_str(created_by, lower=True),
),
).fetchone()
if assignee:
@@ -406,6 +411,12 @@ class ProjectStore:
if "due_date" in fields:
sets.append("due_date = %s")
params.append(store.validate_date(fields["due_date"]))
if "start_time" in fields:
sets.append("start_time = %s")
params.append(store.validate_time(fields["start_time"]))
if "due_time" in fields:
sets.append("due_time = %s")
params.append(store.validate_time(fields["due_time"]))
if "assignee_email" in fields:
new_assignee = store.norm_str(fields["assignee_email"], lower=True)
sets.append("assignee_email = %s")
@@ -714,6 +725,8 @@ class ProjectStore:
for k, v in list(out.items()):
if isinstance(v, datetime):
out[k] = v.astimezone(KST).isoformat(timespec="seconds")
elif isinstance(v, time):
out[k] = v.strftime("%H:%M")
elif isinstance(v, date):
out[k] = v.isoformat()
for k in ("id", "parent_id", "project_id", "stage_id", "task_id", "sort_order"):