-- ===================================================================== -- project_db 초기화 스크립트 (PostgreSQL) — 프로젝트 관리(아사나식) 모듈 -- ===================================================================== -- 멱등(idempotent): 여러 번 실행해도 안전. 기존 데이터를 삭제하지 않는다. -- -- ⚠️ 실행 전 사용자 승인 + 백업 확인 필수. DROP/TRUNCATE 없음. -- -- 실행 방법 (운영 PostgreSQL 컨테이너명: postgres-db): -- -- 1) DB / 역할 / 스키마 생성 (superuser 로 1회) -- read -s -p "project_app password: " APP_PWD; echo -- docker exec -i postgres-db psql -U postgres \ -- -v app_password="$APP_PWD" \ -- < scripts/sql/project_db_init.sql -- -- 2) main-app .env 에 연결 정보 등록 -- PROJECT_DB_URL=postgresql://project_app:@postgres-db:5432/project_db -- -- 3) main-app 재기동 -- cd /opt/www/main && docker compose up -d --build -- -- 주의: -- - 기존 DB 가 있으면 DROP 하지 않는다(CREATE DATABASE 는 미존재 시에만). -- - 비밀번호는 절대 Git 에 커밋하지 않는다. psql -v 로만 전달. -- ===================================================================== \set ON_ERROR_STOP on -- DB 가 없을 때만 생성 SELECT 'CREATE DATABASE project_db ENCODING ''UTF8'' TEMPLATE template0' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'project_db') \gexec -- 앱 전용 로그인 역할 (vacation_app / cupang_app 패턴과 동일) SELECT 'CREATE ROLE project_app LOGIN PASSWORD ' || quote_literal(:'app_password') WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'project_app') \gexec -- 항상 최신 비밀번호로 동기화 SELECT 'ALTER ROLE project_app WITH LOGIN PASSWORD ' || quote_literal(:'app_password') \gexec GRANT CONNECT ON DATABASE project_db TO project_app; -- project_db 컨텍스트로 전환 \connect project_db -- ── updated_at 자동 갱신 트리거 함수 (멱등: CREATE OR REPLACE) ── CREATE OR REPLACE FUNCTION project_set_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at := now(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- ════════════════════════════════════════════════════════════ -- 1) 프로젝트 / 서브프로젝트 (self-referential: parent_id) -- parent_id NULL = 최상위 프로젝트, NOT NULL = 서브프로젝트. -- 삭제는 CASCADE (서브프로젝트·단계·업무 모두 함께 제거). -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS projects ( id BIGSERIAL PRIMARY KEY, parent_id BIGINT REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', color TEXT NOT NULL DEFAULT '#4573d2', owner_email TEXT NOT NULL DEFAULT '', start_date DATE, due_date DATE, status TEXT NOT NULL DEFAULT 'active', -- active | archived sort_order INTEGER NOT NULL DEFAULT 0, created_by TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_projects_parent ON projects (parent_id); CREATE INDEX IF NOT EXISTS idx_projects_status ON projects (status); DROP TRIGGER IF EXISTS trg_projects_updated ON projects; CREATE TRIGGER trg_projects_updated BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION project_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 2) 프로젝트 멤버 배정 (관리자가 사용자를 프로젝트에 지정) -- role: manager(서브프로젝트/업무 관리) | member -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS project_members ( id BIGSERIAL PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, user_email TEXT NOT NULL, user_name TEXT NOT NULL DEFAULT '', role TEXT NOT NULL DEFAULT 'member', -- manager | member created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (project_id, user_email) ); CREATE INDEX IF NOT EXISTS idx_project_members_project ON project_members (project_id); CREATE INDEX IF NOT EXISTS idx_project_members_email ON project_members (user_email); -- ════════════════════════════════════════════════════════════ -- 3) 진행 단계 (칸반 컬럼) — 프로젝트별. 생성 시 기본 4단계 seed. -- is_done_stage: 이 단계로 옮기면 업무 '완료'로 간주(메일 트리거). -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS project_stages ( id BIGSERIAL PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, is_done_stage BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_project_stages_project ON project_stages (project_id, sort_order); -- ════════════════════════════════════════════════════════════ -- 4) 업무 (task) -- stage_id 로 진행단계 표현. assignee_email 로 담당자 지정. -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS tasks ( id BIGSERIAL PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, stage_id BIGINT REFERENCES project_stages(id) ON DELETE SET NULL, title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', assignee_email TEXT NOT NULL DEFAULT '', assignee_name TEXT NOT NULL DEFAULT '', priority TEXT NOT NULL DEFAULT 'normal', -- low | normal | high start_date DATE, due_date DATE, completed_at TIMESTAMPTZ, sort_order INTEGER NOT NULL DEFAULT 0, created_by TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks (project_id, sort_order); CREATE INDEX IF NOT EXISTS idx_tasks_stage ON tasks (stage_id); CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks (assignee_email); CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks (due_date); DROP TRIGGER IF EXISTS trg_tasks_updated ON tasks; CREATE TRIGGER trg_tasks_updated BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION project_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 5) 업무 댓글 (아사나 기본기능 — 스켈레톤 단계 테이블) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS task_comments ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, author_email TEXT NOT NULL DEFAULT '', author_name TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments (task_id, created_at); -- ════════════════════════════════════════════════════════════ -- 6) 활동 이력 (배정/완료 등 — 메일 알림 근거 + 타임라인) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS project_activity ( id BIGSERIAL PRIMARY KEY, project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE, task_id BIGINT REFERENCES tasks(id) ON DELETE CASCADE, actor_email TEXT NOT NULL DEFAULT '', action TEXT NOT NULL, -- assigned | completed | created | stage_changed ... detail TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_project_activity_project ON project_activity (project_id, created_at DESC); -- ════════════════════════════════════════════════════════════ -- 7) 권한 (project_app: CRUD only) -- ════════════════════════════════════════════════════════════ GRANT USAGE ON SCHEMA public TO project_app; GRANT SELECT, INSERT, UPDATE, DELETE ON projects, project_members, project_stages, tasks, task_comments, project_activity TO project_app; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO project_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO project_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO project_app; SELECT 'project_db ready' AS status;