-- ===================================================================== -- 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로 -- 바꿔둔다(개행→
, &/ 이스케이프 — 순서가 중요하다: &부터 바꿔야 한다). UPDATE tasks SET description = replace(replace(replace(replace( description, '&', '&'), '<', '<'), '>', '>'), E'\n', '
') 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;