-- ===================================================================== -- vacation_db 초기화 스크립트 (PostgreSQL) — 휴가 관리 모듈 -- ===================================================================== -- 멱등(idempotent): 여러 번 실행해도 안전. 기존 데이터를 삭제하지 않는다. -- -- ⚠️ 실행 전 사용자 승인 + 백업 확인 필수. DROP/TRUNCATE 없음. -- -- 실행 방법 (운영 PostgreSQL 컨테이너명: postgres-db): -- -- 1) DB / 역할 / 스키마 생성 (superuser 로 1회) -- read -s -p "vacation_app password: " APP_PWD; echo -- docker exec -i postgres-db psql -U postgres \ -- -v app_password="$APP_PWD" \ -- < scripts/sql/vacation_db_init.sql -- -- 2) main-app .env 에 연결 정보 등록 -- VACATION_DB_URL=postgresql://vacation_app:@postgres-db:5432/vacation_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 vacation_db ENCODING ''UTF8'' TEMPLATE template0' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'vacation_db') \gexec -- 앱 전용 로그인 역할 (expense_app / cupang_app 패턴과 동일) SELECT 'CREATE ROLE vacation_app LOGIN PASSWORD ' || quote_literal(:'app_password') WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'vacation_app') \gexec -- 항상 최신 비밀번호로 동기화 SELECT 'ALTER ROLE vacation_app WITH LOGIN PASSWORD ' || quote_literal(:'app_password') \gexec GRANT CONNECT ON DATABASE vacation_db TO vacation_app; -- vacation_db 컨텍스트로 전환 \connect vacation_db -- ── updated_at 자동 갱신 트리거 함수 (멱등: CREATE OR REPLACE) ── CREATE OR REPLACE FUNCTION vacation_set_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at := now(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- ════════════════════════════════════════════════════════════ -- 1) 휴가 신청 (헤더) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS vacation_requests ( id TEXT PRIMARY KEY, owner TEXT NOT NULL, owner_name TEXT NOT NULL DEFAULT '', vacation_type TEXT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, start_half TEXT NOT NULL DEFAULT 'full', end_half TEXT NOT NULL DEFAULT 'full', days NUMERIC(5,2) NOT NULL DEFAULT 0, reason TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT '작성중', approver_email TEXT, decided_at TIMESTAMPTZ, reject_reason TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_vacation_req_owner_start ON vacation_requests (owner, start_date DESC); CREATE INDEX IF NOT EXISTS idx_vacation_req_status ON vacation_requests (status); CREATE INDEX IF NOT EXISTS idx_vacation_req_range ON vacation_requests (start_date, end_date); DROP TRIGGER IF EXISTS trg_vacation_requests_updated ON vacation_requests; CREATE TRIGGER trg_vacation_requests_updated BEFORE UPDATE ON vacation_requests FOR EACH ROW EXECUTE FUNCTION vacation_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 2) 공휴일 (달력 빨강 표시 + 휴가일수 계산 제외 기준) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS vacation_holidays ( id BIGSERIAL PRIMARY KEY, holiday_date DATE NOT NULL UNIQUE, name TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'public', is_red BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_vacation_holidays_date ON vacation_holidays (holiday_date); DROP TRIGGER IF EXISTS trg_vacation_holidays_updated ON vacation_holidays; CREATE TRIGGER trg_vacation_holidays_updated BEFORE UPDATE ON vacation_holidays FOR EACH ROW EXECUTE FUNCTION vacation_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 3) 사용자별 연차 잔여 (연도 단위) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS vacation_balances ( id BIGSERIAL PRIMARY KEY, user_email TEXT NOT NULL, year INTEGER NOT NULL, total_days NUMERIC(5,2) NOT NULL DEFAULT 0, used_days NUMERIC(5,2) NOT NULL DEFAULT 0, memo TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (user_email, year) ); CREATE INDEX IF NOT EXISTS idx_vacation_balances_user_year ON vacation_balances (user_email, year); DROP TRIGGER IF EXISTS trg_vacation_balances_updated ON vacation_balances; CREATE TRIGGER trg_vacation_balances_updated BEFORE UPDATE ON vacation_balances FOR EACH ROW EXECUTE FUNCTION vacation_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 4) 공휴일 seed — 2026년 대한민국 공휴일 (멱등: ON CONFLICT DO NOTHING) -- 연도별로 달라지므로 settings 화면에서 추가/수정/삭제 가능. -- KASI(한국천문연구원) 발표 기준. 새 연도는 settings 또는 본 seed 추가. -- ════════════════════════════════════════════════════════════ INSERT INTO vacation_holidays (holiday_date, name, kind) VALUES ('2026-01-01', '신정', 'public'), ('2026-02-16', '설날 연휴', 'lunar'), ('2026-02-17', '설날', 'lunar'), ('2026-02-18', '설날 연휴', 'lunar'), ('2026-03-01', '삼일절', 'public'), ('2026-03-02', '삼일절 대체', 'substitute'), ('2026-05-05', '어린이날', 'public'), ('2026-05-24', '부처님오신날', 'lunar'), ('2026-05-25', '부처님오신날 대체', 'substitute'), ('2026-06-06', '현충일', 'public'), ('2026-08-15', '광복절', 'public'), ('2026-08-17', '광복절 대체', 'substitute'), ('2026-09-24', '추석 연휴', 'lunar'), ('2026-09-25', '추석', 'lunar'), ('2026-09-26', '추석 연휴', 'lunar'), ('2026-09-28', '추석 대체', 'substitute'), ('2026-10-03', '개천절', 'public'), ('2026-10-05', '개천절 대체', 'substitute'), ('2026-10-09', '한글날', 'public'), ('2026-12-25', '성탄절', 'public') ON CONFLICT (holiday_date) DO NOTHING; -- ════════════════════════════════════════════════════════════ -- 5) 권한 (vacation_app: CRUD only) -- ════════════════════════════════════════════════════════════ GRANT USAGE ON SCHEMA public TO vacation_app; GRANT SELECT, INSERT, UPDATE, DELETE ON vacation_requests, vacation_holidays, vacation_balances TO vacation_app; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO vacation_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO vacation_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO vacation_app; SELECT 'vacation_db ready' AS status;