Initial commit: AI platform app (server, views, lib, data, deploy docs)
Made-with: Cursor
This commit is contained in:
19
db/bootstrap-role.sql.example
Normal file
19
db/bootstrap-role.sql.example
Normal file
@@ -0,0 +1,19 @@
|
||||
-- PostgreSQL 슈퍼유저(예: postgres)로 **한 번만** 실행하는 예시입니다.
|
||||
-- 값을 바꾼 뒤: psql -U postgres -f bootstrap-role.sql (또는 psql 안에서 붙여넣기)
|
||||
--
|
||||
-- 앱은 .env의 DB_USERNAME / DB_PASSWORD / DB_DATABASE 로 접속합니다.
|
||||
-- 역할이 이미 있으면 CREATE ROLE 부분은 생략하고 비밀번호만 맞춥니다: \password 역할이름
|
||||
|
||||
-- 1) 앱 전용 로그인 역할 (이름은 .env의 DB_USERNAME과 동일하게)
|
||||
CREATE ROLE app_user WITH LOGIN PASSWORD '여기에_강한_비밀번호';
|
||||
|
||||
-- 2) 데이터베이스 (.env의 DB_DATABASE와 동일)
|
||||
CREATE DATABASE app_database OWNER app_user;
|
||||
|
||||
-- 이미 DB가 있고 소유자만 바꾸려면(주의: 운영 정책에 맞게 사용):
|
||||
-- ALTER DATABASE app_database OWNER TO app_user;
|
||||
|
||||
-- 3) 같은 서버에서 DB를 만든 직후, public 스키마 권한(버전에 따라 필요)
|
||||
\c app_database
|
||||
GRANT ALL ON SCHEMA public TO app_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO app_user;
|
||||
3
db/migrations/add-lecture-type-video.sql
Normal file
3
db/migrations/add-lecture-type-video.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- 기존 DB에 학습센터 강의 타입 'video' 추가 (PostgreSQL)
|
||||
ALTER TABLE lectures DROP CONSTRAINT IF EXISTS lectures_type_check;
|
||||
ALTER TABLE lectures ADD CONSTRAINT lectures_type_check CHECK (type IN ('youtube', 'ppt', 'news', 'link', 'video'));
|
||||
272
db/schema.sql
Normal file
272
db/schema.sql
Normal file
@@ -0,0 +1,272 @@
|
||||
CREATE TABLE IF NOT EXISTS lectures (
|
||||
id UUID PRIMARY KEY,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('youtube', 'ppt', 'news', 'link', 'video')),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
youtube_url TEXT,
|
||||
file_name TEXT,
|
||||
original_name TEXT,
|
||||
preview_title TEXT,
|
||||
slide_count INTEGER NOT NULL DEFAULT 0,
|
||||
thumbnail_url TEXT,
|
||||
thumbnail_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
thumbnail_retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
thumbnail_error TEXT,
|
||||
thumbnail_updated_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
list_section VARCHAR(20) NOT NULL DEFAULT 'learning',
|
||||
news_url TEXT
|
||||
);
|
||||
|
||||
ALTER TABLE lectures DROP CONSTRAINT IF EXISTS lectures_type_check;
|
||||
ALTER TABLE lectures ADD CONSTRAINT lectures_type_check CHECK (type IN ('youtube', 'ppt', 'news', 'link', 'video'));
|
||||
ALTER TABLE lectures ADD COLUMN IF NOT EXISTS list_section VARCHAR(20) NOT NULL DEFAULT 'learning';
|
||||
ALTER TABLE lectures ADD COLUMN IF NOT EXISTS news_url TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_lectures_type_created_at ON lectures (type, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_lectures_created_at ON lectures (created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_lectures_tags ON lectures USING GIN (tags);
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_lectures_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_lectures_updated_at ON lectures;
|
||||
CREATE TRIGGER trg_lectures_updated_at
|
||||
BEFORE UPDATE ON lectures
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_lectures_updated_at();
|
||||
|
||||
-- AX 과제 신청 (PDF 양식 기반)
|
||||
CREATE TABLE IF NOT EXISTS ax_assignments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
department VARCHAR(200) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
employee_id VARCHAR(50),
|
||||
position VARCHAR(100),
|
||||
phone VARCHAR(50),
|
||||
email VARCHAR(200),
|
||||
work_process_description TEXT,
|
||||
pain_point TEXT,
|
||||
current_time_spent VARCHAR(100),
|
||||
error_rate_before VARCHAR(100),
|
||||
collaboration_depts TEXT,
|
||||
reason_to_solve TEXT,
|
||||
ai_expectation TEXT,
|
||||
output_type TEXT,
|
||||
automation_level VARCHAR(50),
|
||||
data_readiness VARCHAR(50),
|
||||
data_location TEXT,
|
||||
personal_info VARCHAR(50),
|
||||
data_quality VARCHAR(50),
|
||||
data_count VARCHAR(100),
|
||||
data_types TEXT[],
|
||||
time_reduction VARCHAR(100),
|
||||
error_reduction VARCHAR(100),
|
||||
volume_increase VARCHAR(100),
|
||||
cost_reduction VARCHAR(100),
|
||||
response_time VARCHAR(100),
|
||||
other_metrics TEXT,
|
||||
annual_savings VARCHAR(100),
|
||||
labor_replacement VARCHAR(100),
|
||||
revenue_increase VARCHAR(100),
|
||||
other_effects TEXT,
|
||||
qualitative_effects TEXT[],
|
||||
tech_stack TEXT[],
|
||||
risks TEXT[],
|
||||
risk_detail TEXT,
|
||||
participation_pledge BOOLEAN DEFAULT false,
|
||||
application_file TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT '신청',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE ax_assignments ADD COLUMN IF NOT EXISTS application_file TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ax_assignments_created_at ON ax_assignments (created_at DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_ax_assignments_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_ax_assignments_updated_at ON ax_assignments;
|
||||
CREATE TRIGGER trg_ax_assignments_updated_at
|
||||
BEFORE UPDATE ON ax_assignments
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_ax_assignments_updated_at();
|
||||
|
||||
-- OPS 이메일(@xavis.co.kr) 매직 링크 인증 — 이벤트 감사 로그
|
||||
CREATE TABLE IF NOT EXISTS ops_email_auth_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(320) NOT NULL,
|
||||
event_type VARCHAR(40) NOT NULL CHECK (event_type IN ('magic_link_requested', 'login_success', 'logout')),
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
return_to TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ops_email_auth_events_email_created ON ops_email_auth_events (email, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ops_email_auth_events_created ON ops_email_auth_events (created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ops_email_auth_events_event_type ON ops_email_auth_events (event_type);
|
||||
|
||||
-- 이메일별 최초·최근 로그인 및 누적 로그인 횟수
|
||||
CREATE TABLE IF NOT EXISTS ops_email_users (
|
||||
email VARCHAR(320) PRIMARY KEY,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_login_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
login_count INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ops_email_users_last_login ON ops_email_users (last_login_at DESC);
|
||||
|
||||
-- 회의록 AI: 이메일(OPS 세션) 기반 사용자·프롬프트·회의 저장
|
||||
CREATE TABLE IF NOT EXISTS meeting_ai_users (
|
||||
email VARCHAR(320) PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meeting_ai_prompts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_email VARCHAR(320) NOT NULL REFERENCES meeting_ai_users(email) ON DELETE CASCADE,
|
||||
include_title_line BOOLEAN NOT NULL DEFAULT true,
|
||||
include_attendees BOOLEAN NOT NULL DEFAULT true,
|
||||
include_summary BOOLEAN NOT NULL DEFAULT true,
|
||||
include_action_items BOOLEAN NOT NULL DEFAULT true,
|
||||
include_checklist BOOLEAN NOT NULL DEFAULT true,
|
||||
custom_instructions TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_meeting_ai_prompts_user UNIQUE (user_email)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_meeting_ai_prompts_user ON meeting_ai_prompts (user_email);
|
||||
|
||||
-- 기존 DB: 신규 행 기본값만 갱신(이미 저장된 include_checklist 값은 유지)
|
||||
ALTER TABLE meeting_ai_prompts ALTER COLUMN include_checklist SET DEFAULT true;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meeting_ai_meetings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_email VARCHAR(320) NOT NULL REFERENCES meeting_ai_users(email) ON DELETE CASCADE,
|
||||
title VARCHAR(500) NOT NULL DEFAULT '',
|
||||
source_text TEXT,
|
||||
transcript_text TEXT,
|
||||
generated_minutes TEXT,
|
||||
summary_text TEXT,
|
||||
audio_file_path TEXT,
|
||||
audio_original_name TEXT,
|
||||
chat_model VARCHAR(80) NOT NULL DEFAULT 'gpt-5-mini',
|
||||
transcription_model VARCHAR(80),
|
||||
meeting_date DATE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_meeting_ai_meetings_user_created ON meeting_ai_meetings (user_email, created_at DESC);
|
||||
|
||||
-- 기존 DB 마이그레이션: 전사에 사용한 OpenAI 모델(gpt-4o-mini-transcribe, gpt-4o-transcribe 등)
|
||||
ALTER TABLE meeting_ai_meetings ADD COLUMN IF NOT EXISTS transcription_model VARCHAR(80);
|
||||
|
||||
-- 미팅 일자(회의가 열린 날짜, 선택)
|
||||
ALTER TABLE meeting_ai_meetings ADD COLUMN IF NOT EXISTS meeting_date DATE;
|
||||
|
||||
-- 회의록 생성 후 체크리스트 자동 추출(JSON 스냅샷, 업무 체크리스트 AI 연동)
|
||||
ALTER TABLE meeting_ai_meetings ADD COLUMN IF NOT EXISTS checklist_snapshot JSONB;
|
||||
|
||||
-- 업무 체크리스트 툴팁용 짧은 요약(생성 시 회의록 본문에서 추출해 저장, 없으면 조회 시 추출)
|
||||
ALTER TABLE meeting_ai_meetings ADD COLUMN IF NOT EXISTS summary_text TEXT;
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_meeting_ai_prompts_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_meeting_ai_prompts_updated_at ON meeting_ai_prompts;
|
||||
CREATE TRIGGER trg_meeting_ai_prompts_updated_at
|
||||
BEFORE UPDATE ON meeting_ai_prompts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_meeting_ai_prompts_updated_at();
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_meeting_ai_meetings_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_meeting_ai_meetings_updated_at ON meeting_ai_meetings;
|
||||
CREATE TRIGGER trg_meeting_ai_meetings_updated_at
|
||||
BEFORE UPDATE ON meeting_ai_meetings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_meeting_ai_meetings_updated_at();
|
||||
|
||||
-- 업무 체크리스트 AI: 사용자별 항목(회의록에서 가져오기 또는 수동)
|
||||
CREATE TABLE IF NOT EXISTS meeting_ai_checklist_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_email VARCHAR(320) NOT NULL REFERENCES meeting_ai_users(email) ON DELETE CASCADE,
|
||||
meeting_id UUID REFERENCES meeting_ai_meetings(id) ON DELETE SET NULL,
|
||||
title TEXT NOT NULL,
|
||||
detail TEXT,
|
||||
assignee VARCHAR(300),
|
||||
due_note VARCHAR(300),
|
||||
completed BOOLEAN NOT NULL DEFAULT false,
|
||||
completed_at TIMESTAMPTZ,
|
||||
completion_note TEXT,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
source VARCHAR(20) NOT NULL DEFAULT 'imported' CHECK (source IN ('imported', 'manual')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE meeting_ai_checklist_items ADD COLUMN IF NOT EXISTS completion_note TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_meeting_ai_checklist_user_updated ON meeting_ai_checklist_items (user_email, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_meeting_ai_checklist_meeting ON meeting_ai_checklist_items (meeting_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_meeting_ai_checklist_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_meeting_ai_checklist_updated_at ON meeting_ai_checklist_items;
|
||||
CREATE TRIGGER trg_meeting_ai_checklist_updated_at
|
||||
BEFORE UPDATE ON meeting_ai_checklist_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_meeting_ai_checklist_updated_at();
|
||||
|
||||
-- PPT 썸네일 작업 이벤트 로그 (기존 data/thumbnail-events.json 대체)
|
||||
CREATE TABLE IF NOT EXISTS lecture_thumbnail_events (
|
||||
id UUID PRIMARY KEY,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
event_type VARCHAR(40) NOT NULL,
|
||||
lecture_id UUID,
|
||||
lecture_title TEXT,
|
||||
reason VARCHAR(200),
|
||||
force_flag BOOLEAN NOT NULL DEFAULT false,
|
||||
queue_size_after INTEGER,
|
||||
retry_count INTEGER,
|
||||
duration_ms INTEGER,
|
||||
error_text TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_lecture_thumbnail_events_occurred ON lecture_thumbnail_events (occurred_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_lecture_thumbnail_events_type ON lecture_thumbnail_events (event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_lecture_thumbnail_events_lecture ON lecture_thumbnail_events (lecture_id);
|
||||
Reference in New Issue
Block a user