Files
dbx-main/scripts/sql/expense_db_init.sql
T
king 550867ba0c feat(expense+admin): 결재 워크플로 + 첨부 + 집계 + 엑셀 + 사용자 직접 등록
[admin]
- POST /api/users 사용자 직접 등록 (이메일만으로). UserStore.create_user 추가
- admin.html: 등록 폼 + 삭제 버튼 + 한글 라벨 + 신규 권한 표시
- 신규 권한 키: expense_approver, vacation_approver (APPROVER_KEYS)
- is_approver(user_rec, kind) 헬퍼

[expense 워크플로]
- 상태 전이: 작성중 → 제출 → 승인/반려 → 정산완료
- API: /api/items/{id}/{submit,revert,approve,reject,settle}
- 권한: submit/revert=owner, approve/reject/settle=expense_approver 또는 admin
- expense_items 컬럼: approver_email, decided_at, reject_reason
- 작성중/반려 상태에서만 수정/삭제/첨부 가능

[expense 첨부]
- 영수증/기타 (kind: receipt|other). 최대 20MB, 확장자 화이트리스트
- 저장: DATA_DIR/uploads/expense/{item_id}/{att_id}_{filename}
- expense_attachments 테이블 (CASCADE on item delete)
- API: 업로드/목록/다운로드/삭제

[expense 집계]
- /expense/reports: 연도별 월×카테고리 피벗 표

[expense 엑셀]
- GET /expense/api/export.xlsx?from&to&scope=mine|all
- scope=all 은 승인자/관리자만

[DDL]
- expense_db_init.sql: 신규 컬럼/테이블 포함 (멱등)
- expense_db_002_workflow_attachments.sql: 운영 DB 마이그레이션용

[deps]
- openpyxl>=3.1, python-multipart>=0.0.20

[docs]
- DATABASES.md: 워크플로 다이어그램, attachments 테이블, 마이그레이션
- PROJECT_OVERVIEW.md: 권한 키 표
- DEPLOYMENT.md: DATA_DIR/uploads 안내
2026-05-29 02:52:44 +09:00

108 lines
4.5 KiB
PL/PgSQL

-- =====================================================================
-- expense_db 초기화 스크립트 (PostgreSQL)
-- =====================================================================
-- 실행 방법 (운영 PostgreSQL 컨테이너명: postgres-db):
--
-- 1) DB / 역할 생성 (superuser 로 1회)
-- docker exec -i postgres-db psql -U postgres -v app_password='<강한비밀번호>' \
-- < scripts/sql/expense_db_init.sql
--
-- 2) main-app .env 에 연결 정보 등록
-- EXPENSE_DB_URL=postgresql://expense_app:<강한비밀번호>@postgres-db:5432/expense_db
--
-- 주의:
-- - 기존 DB 가 있으면 DROP 하지 않는다. CREATE DATABASE 만 IF NOT EXISTS 대체.
-- - 비밀번호는 절대 Git 에 커밋하지 않는다. psql -v 또는 \set 으로만 전달.
-- - DROP/TRUNCATE 가 필요한 경우 본 스크립트를 수정하지 말고 별도 작업으로 진행.
-- =====================================================================
\set ON_ERROR_STOP on
-- DB 가 없을 때만 생성
SELECT 'CREATE DATABASE expense_db ENCODING ''UTF8'' TEMPLATE template0'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'expense_db')
\gexec
-- 앱 전용 로그인 역할
-- psql 변수(:'app_password')는 dollar-quoted 블록 안에서 치환되지 않으므로
-- DO 블록을 쓰지 않고 \gexec 로 동적 SQL 을 생성·실행한다.
SELECT 'CREATE ROLE expense_app LOGIN PASSWORD ' || quote_literal(:'app_password')
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'expense_app')
\gexec
-- 항상 최신 비밀번호로 동기화
SELECT 'ALTER ROLE expense_app WITH LOGIN PASSWORD ' || quote_literal(:'app_password')
\gexec
GRANT CONNECT ON DATABASE expense_db TO expense_app;
-- 이제 expense_db 컨텍스트로 전환
\connect expense_db
CREATE TABLE IF NOT EXISTS expense_items (
id TEXT PRIMARY KEY,
owner TEXT NOT NULL,
spent_at DATE NOT NULL,
category TEXT NOT NULL,
method TEXT NOT NULL,
merchant TEXT NOT NULL DEFAULT '',
amount BIGINT NOT NULL DEFAULT 0 CHECK (amount >= 0),
memo TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT '작성중',
approver_email TEXT,
decided_at TIMESTAMPTZ,
reject_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 기존 DB 호환: 컬럼이 없으면 추가
ALTER TABLE expense_items
ADD COLUMN IF NOT EXISTS approver_email TEXT,
ADD COLUMN IF NOT EXISTS decided_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS reject_reason TEXT;
CREATE INDEX IF NOT EXISTS idx_expense_owner_spent ON expense_items (owner, spent_at DESC);
CREATE INDEX IF NOT EXISTS idx_expense_status ON expense_items (status);
CREATE INDEX IF NOT EXISTS idx_expense_created_at ON expense_items (created_at DESC);
-- 첨부파일 (영수증/기타). 실제 파일은 파일시스템 저장, 메타만 DB.
CREATE TABLE IF NOT EXISTS expense_attachments (
id TEXT PRIMARY KEY,
item_id TEXT NOT NULL REFERENCES expense_items(id) ON DELETE CASCADE,
owner TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('receipt', 'other')),
filename TEXT NOT NULL,
stored_path TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes BIGINT NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_attach_item ON expense_attachments (item_id);
-- updated_at 자동 갱신 트리거
CREATE OR REPLACE FUNCTION expense_set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_expense_set_updated_at ON expense_items;
CREATE TRIGGER trg_expense_set_updated_at
BEFORE UPDATE ON expense_items
FOR EACH ROW EXECUTE FUNCTION expense_set_updated_at();
-- 권한
GRANT USAGE ON SCHEMA public TO expense_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON expense_items TO expense_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON expense_attachments TO expense_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO expense_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO expense_app;
-- 확인용 출력
SELECT 'expense_db ready' AS status;