07f4d0fd5b
프로젝트→세션→업무 3단계(아사나식) 전환의 1단계(백엔드). "세션"은 새 개념이
아니라 기존 "단계"(project_stages, 칸반 컬럼)를 그대로 재사용한다.
- project_stages 에 created_by 추가 — 세션 삭제도 "만든 사람만"(_can_delete
재사용) 규칙 적용. api_delete_stage 를 _require_manage 단독 체크에서
생성자 체크로 변경.
- 프로젝트 생성을 관리자 전용 → 로그인 사용자 전체로 개방
(api_create_project: _require_admin → _require_user). 홈 화면의 "새
프로젝트"/수정 모달·버튼도 is_admin 게이트를 걷어냄(생성자가 자동으로
owner 가 되어 _can_manage 통과 — 기존 로직 재사용). 멤버 배정 API는 계속
관리자 전용이라, 비관리자가 새 프로젝트 모달을 열면 멤버 선택란은 안내
문구만 보여주고 시도하지 않는다(불필요한 403 방지).
- 하위 업무 — 새 테이블 없이 tasks.parent_task_id 재사용(부모 삭제 시
CASCADE). list_tasks 는 기본적으로 최상위만 반환(보드/캘린더/리스트에
하위업무가 카드로 안 뜨게) + comment_count 와 같은 방식으로 subtask_total/
subtask_done 집계 추가. create_task 에 parent_task_id 지원(하위업무는
부모와 같은 프로젝트, 세션 없음).
- 멀티호밍(task_project_links, 아사나 기능) — 업무 하나가 여러 프로젝트/세션에
동시에 속할 수 있다. list_tasks(project_id=X, 비재귀)가 자동으로 그
프로젝트에 연결된 업무까지 포함하도록 확장(다른 호출 경로는 안 건드림 —
홈/내업무 전체보기에서 중복 노출 방지). 신규 API: GET /api/tasks/{id}(하위
업무·연결 포함 상세), POST/DELETE /api/tasks/{id}/links.
- 설명란 이미지 삽입 준비: store.sanitize_description_html — 허용 태그만
남기고 스크립트/이벤트속성/위험 URL 스킴 제거(표준 html.parser, 외부
라이브러리 없음). api_create_task/api_update_task 저장 직전 통과.
⚠️ 배포 전 필수: scripts/sql/project_db_003_sections_subtasks_links.sql 를
project_db 에 적용해야 한다(project_stages.created_by, tasks.parent_task_id,
task_project_links 없이는 이 커밋의 쿼리가 전부 실패한다). 그 안의 기존
설명 텍스트→HTML 이스케이프 UPDATE 문은 1회만 실행할 것.
프론트엔드(세션 드래그 재정렬, 사이드바 프로젝트별 보기, 하위업무/멀티호밍
UI, 설명란 이미지 붙여넣기 편집기)는 다음 커밋에서 이어간다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
60 lines
3.9 KiB
SQL
60 lines
3.9 KiB
SQL
-- =====================================================================
|
|
-- project_db 003 — 세션(단계) 삭제자 기록 · 하위 업무 · 프로젝트 간 멀티호밍
|
|
-- =====================================================================
|
|
-- 멱등(idempotent): 여러 번 실행해도 안전하다 — 단, 아래 UPDATE(설명 이스케이프)는
|
|
-- 예외다. 그 문장은 반드시 1회만 실행할 것(주석 참고). DROP/TRUNCATE 없음.
|
|
--
|
|
-- 실행:
|
|
-- docker exec -i postgres-db psql -U postgres -d project_db \
|
|
-- < scripts/sql/project_db_003_sections_subtasks_links.sql
|
|
--
|
|
-- 배경:
|
|
-- 프로젝트 → 세션(기존 "단계"를 그대로 재사용) → 업무 3단계 구조로 바뀌면서
|
|
-- 1) 세션도 "만든 사람만 삭제" 규칙을 적용하려면 만든 사람을 기록해야 한다.
|
|
-- 2) 업무는 하위 업무(subtask)를 가질 수 있어야 한다 — tasks 테이블 자체를
|
|
-- 재사용한다(parent_task_id 만 다르다. 부모 삭제 시 CASCADE).
|
|
-- 3) 업무 하나가 여러 프로젝트/세션에 동시에 속하는 멀티호밍(아사나 기능)을
|
|
-- 지원해야 한다 — 원래 소속(tasks.project_id/stage_id)은 "기본 홈"으로 두고,
|
|
-- 추가 소속만 담는 새 표를 둔다.
|
|
-- =====================================================================
|
|
|
|
\set ON_ERROR_STOP on
|
|
|
|
ALTER TABLE project_stages ADD COLUMN IF NOT EXISTS created_by TEXT NOT NULL DEFAULT '';
|
|
|
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS parent_task_id BIGINT REFERENCES tasks(id) ON DELETE CASCADE;
|
|
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks (parent_task_id);
|
|
|
|
-- ⚠️ 1회성 데이터 변환 — 반드시 한 번만 실행할 것(두 번 실행하면 이중 이스케이프됨).
|
|
-- 지금까지 tasks.description 은 일반 텍스트였다. 앞으로 화면은 이 컬럼을 항상
|
|
-- HTML로 취급한다(붙여넣기 이미지 삽입을 지원하기 위해 — app/modules/project/
|
|
-- store.py 의 sanitize_description_html 참고). 런타임에 "이게 HTML이냐 텍스트냐"
|
|
-- 판단하는 방식은 오탐이 나므로 쓰지 않고, 기존 값을 여기서 한 번에 안전한 HTML로
|
|
-- 바꿔둔다(개행→<br>, &/</> 이스케이프 — 순서가 중요하다: &부터 바꿔야 한다).
|
|
UPDATE tasks
|
|
SET description = replace(replace(replace(replace(
|
|
description, '&', '&'), '<', '<'), '>', '>'), E'\n', '<br>')
|
|
WHERE description IS NOT NULL AND description <> '';
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 멀티호밍 — 업무 1개가 여러 프로젝트(각각 세션 지정)에 동시에 속함.
|
|
-- 원래 소속(tasks.project_id/stage_id)은 그대로 "기본 홈"이고, 이 표는 "추가
|
|
-- 홈"만 담는다. 기본 홈은 이 표로 지우지 못한다(업무 삭제로만 없어진다).
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS task_project_links (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
task_id BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
stage_id BIGINT REFERENCES project_stages(id) ON DELETE SET NULL,
|
|
created_by TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (task_id, project_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_task_links_task ON task_project_links (task_id);
|
|
CREATE INDEX IF NOT EXISTS idx_task_links_project ON task_project_links (project_id, stage_id);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON task_project_links TO project_app;
|
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO project_app;
|
|
|
|
SELECT 'project_db 003 applied' AS status;
|