-- AI DEV portal 스키마 (search_path=portal). 멱등. -- 직원 식별자는 사번(Keycloak preferred_username). 표시 이름은 name. CREATE TABLE IF NOT EXISTS users ( sabun text PRIMARY KEY, -- 사번 (Keycloak preferred_username) name text NOT NULL, email text, avatar_seed text, -- 아바타 색/이니셜용(미업로드 시 identicon 시드) created_at timestamptz NOT NULL DEFAULT now(), last_login timestamptz ); -- 업로드한 프로필 사진(1:1 크롭·256px webp 등, 브라우저에서 축소). 없으면 identicon 폴백. ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar bytea; ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_mime text; ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_updated_at timestamptz; -- 한줄 소개(프로필에 표시). 본인만 편집. ALTER TABLE users ADD COLUMN IF NOT EXISTS bio text DEFAULT ''; -- 관리자 여부. Keycloak 그룹 기준으로 로그인 시마다 갱신(소스 오브 트루스는 Keycloak). ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin boolean NOT NULL DEFAULT false; -- 공유 프로젝트 (직원이 만든 앱/repo 를 포털에 공유) CREATE TABLE IF NOT EXISTS projects ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, owner_sabun text NOT NULL REFERENCES users(sabun), title text NOT NULL, description text DEFAULT '', repo_url text DEFAULT '', -- Gitea repo app_url text DEFAULT '', -- 배포된 앱 (*.apps.bokdev.in) tags text DEFAULT '', -- 콤마 구분 is_public boolean NOT NULL DEFAULT true, -- 공개(피드 노출) / 비공개(나만) lang text DEFAULT '', -- 대표 언어(태그에서 추론, 카드 언어 점) created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -- 기존 배포 DB 대비 멱등 컬럼 추가 ALTER TABLE projects ADD COLUMN IF NOT EXISTS is_public boolean NOT NULL DEFAULT true; ALTER TABLE projects ADD COLUMN IF NOT EXISTS lang text DEFAULT ''; CREATE INDEX IF NOT EXISTS idx_projects_created ON projects(created_at DESC); -- 코멘트 (parent_id 가 null 이면 최상위, 있으면 대댓글 — 1뎁스만 허용: 앱에서 검증) CREATE TABLE IF NOT EXISTS comments ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id bigint NOT NULL REFERENCES projects(id) ON DELETE CASCADE, author_sabun text NOT NULL REFERENCES users(sabun), parent_id bigint REFERENCES comments(id) ON DELETE CASCADE, body text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE comments ADD COLUMN IF NOT EXISTS parent_id bigint REFERENCES comments(id) ON DELETE CASCADE; CREATE INDEX IF NOT EXISTS idx_comments_project ON comments(project_id, created_at); -- 좋아요(❤️) — 1인 1좋아요 CREATE TABLE IF NOT EXISTS hearts ( project_id bigint NOT NULL REFERENCES projects(id) ON DELETE CASCADE, sabun text NOT NULL REFERENCES users(sabun), created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (project_id, sabun) ); -- 즐겨찾기(⭐ Star) — 1인 1즐겨찾기 CREATE TABLE IF NOT EXISTS stars ( project_id bigint NOT NULL REFERENCES projects(id) ON DELETE CASCADE, sabun text NOT NULL REFERENCES users(sabun), created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (project_id, sabun) ); -- 공지 (관리자가 작성해 전 직원에게 알림으로 전달. 별도 모아보기 페이지 /announcements) CREATE TABLE IF NOT EXISTS announcements ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, author_sabun text NOT NULL REFERENCES users(sabun), title text NOT NULL, body text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_announcements_created ON announcements(created_at DESC); -- 알림 (통합 인박스) — 반응(❤️/⭐)·댓글·공지를 한 테이블로. -- recipient_sabun: 받는 사람 / actor_sabun: 행위자(공지는 작성 관리자, 시스템성이면 NULL 가능) -- kind: 'heart' | 'star' | 'comment' | 'announcement' -- project_id / announcement_id: 알림이 가리키는 대상(각각 삭제 시 CASCADE 로 정리) CREATE TABLE IF NOT EXISTS notifications ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, recipient_sabun text NOT NULL REFERENCES users(sabun), actor_sabun text REFERENCES users(sabun), kind text NOT NULL, project_id bigint REFERENCES projects(id) ON DELETE CASCADE, announcement_id bigint REFERENCES announcements(id) ON DELETE CASCADE, is_read boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now() ); -- 종 드롭다운/배지 조회: 받는 사람별 최신순. CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_sabun, created_at DESC);