feat(project): 세션 헤더 색상 채우기, 보드 댓글팝업, 채팅 첨부파일/이미지

- 세션 색상: 점 대신 세션 헤더 윗부분 배경을 통째로 칠함(글자/카운트 흰색으로)
- 보드 업무 카드의 댓글 말풍선도 더블클릭하면 채팅 팝업 열리게(.pj-cmt-badge 공용)
- 댓글 채팅 팝업에 파일/이미지 첨부 추가: 클립 버튼 업로드 + 클립보드 이미지
  붙여넣기, 이미지는 썸네일, 파일은 확장자 아이콘+파일명으로 표시, 다운로드
  가능, 업로드한 사람만 삭제(즉시 반영). task_attachments.comment_id(마이그
  레이션 005)로 태깅해 업무 "첨부파일" 패널에도 그대로 함께 나온다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 12:27:44 +09:00
parent 6922679f51
commit 1d7fe71b75
8 changed files with 258 additions and 27 deletions
+16 -5
View File
@@ -683,11 +683,21 @@ class ProjectStore:
).fetchall()
return [self._serialize(r) for r in rows]
def get_comment(self, *, comment_id: int) -> dict[str, Any] | None:
with self._pool.connection() as conn:
row = conn.execute(
"SELECT * FROM task_comments WHERE id = %s", (comment_id,)
).fetchone()
return self._serialize(row) if row else None
def add_comment(
self, *, task_id: int, author_email: str, author_name: str, body: str
self, *, task_id: int, author_email: str, author_name: str, body: str,
allow_empty: bool = False,
) -> dict[str, Any]:
"""allow_empty — 채팅 팝업에서 파일/이미지만 보내는 메시지(본문 없음)를
허용할 때 True. 일반 댓글 입력은 여전히 빈 값을 막는다."""
text = store.norm_str(body)
if not text:
if not text and not allow_empty:
raise ValueError("댓글 내용이 비었습니다.")
with self._pool.connection() as conn:
row = conn.execute(
@@ -744,15 +754,16 @@ class ProjectStore:
def add_attachment(
self, *, task_id: int, filename: str, stored_name: str,
content_type: str, size_bytes: int, uploaded_by: str,
comment_id: int | None = None,
) -> dict[str, Any]:
with self._pool.connection() as conn:
row = conn.execute(
"INSERT INTO task_attachments "
"(task_id, filename, stored_name, content_type, size_bytes, uploaded_by) "
"VALUES (%s,%s,%s,%s,%s,%s) RETURNING *",
"(task_id, filename, stored_name, content_type, size_bytes, uploaded_by, comment_id) "
"VALUES (%s,%s,%s,%s,%s,%s,%s) RETURNING *",
(task_id, store.norm_str(filename), stored_name,
store.norm_str(content_type), int(size_bytes),
store.norm_str(uploaded_by, lower=True)),
store.norm_str(uploaded_by, lower=True), comment_id),
).fetchone()
return self._serialize(row)