# Databases ## PostgreSQL DB 목록 | DB명 | 용도 | | --- | --- | | `itemcode_db` | 상품코드, 단품/세트 구성, 채널 ↔ 사내 코드 매칭 | | `orderlist_db` | 주문 수집·분석·관리 (구 `orderlist_app`) | | `return_db` | 반품·교환·CS 데이터 | | `expense_db` | 개인경비 / 법인카드 사용내역 / 정산 | | `cupang_db` | 쿠팡 밀크런 출고 묶음 / 출고 라인 / 입고센터 / 박스 입수량 규칙 | | `malaysia_stock_db` | 말레이시아 창고 재고관리 — 창고/아이템/세트 BOM/입출고 이력/일일 재고조사 | --- ## 명명 규칙 - 모든 DB 이름은 **소문자 + 언더스코어**, **`_db`로 끝낸다**. - ✅ `inventory_db`, `cs_db` - ❌ `inventoryApp`, `cs-database` - 신규 DB가 필요하면 **사용자 승인 후** 생성한다. - 신규 DB 생성 시 함께 정리할 항목: - 용도와 책임 모듈 - 소유자(OWNER) 계정 - 백업 주기와 위치 - `.env`의 연결 정보 변수명 --- ## 레거시 이름 매핑 | 예전 이름 | 현재 기준 | | --- | --- | | `orderlist_app` | `orderlist_db` | 코드/문서/설정에서 `orderlist_app`을 발견하면 `orderlist_db`로 수정한다 (수정 전 영향 범위 확인). --- ## DB 작업 원칙 1. **작업 전 백업 우선**. 백업 없는 변경은 진행하지 않는다. 2. 테이블 owner, 권한, sequence 권한을 확인한다. 3. 운영 DB의 `DROP`, `TRUNCATE`, 조건 없는 대량 `DELETE/UPDATE`는 **사용자 확인 없이 실행 금지**. 4. 스키마 변경은 마이그레이션 스크립트(`scripts/` 또는 alembic 등)로 관리한다. 5. 운영 DB와 개발 DB의 접속 정보를 혼동하지 않는다 (`.env`로 분리). --- ## 위험 명령 (사용자 승인 필수) | 명령 | 비고 | | --- | --- | | `DROP DATABASE` | 복구 불가. 백업 없으면 절대 실행 금지 | | `DROP TABLE` / `DROP SCHEMA` | 의존 객체 확인 필수 | | `TRUNCATE` | FK CASCADE 시 광범위 삭제 위험 | | 조건 없는 `DELETE` / `UPDATE` | `WHERE` 없는 문 차단 | | `docker volume rm ` | 운영 데이터 영구 손실 | | `docker compose down -v` | 볼륨까지 제거. 운영에서 금지 | 실행 전 반드시: 1. 백업 확인 (`pg_dump`, 컨테이너 외부 마운트) 2. 영향 범위 설명 3. 사용자 명시 승인 --- ## 자주 쓰는 점검 명령 ```bash # 컨테이너/네트워크 docker ps docker network ls docker inspect # DB 목록 / 접속 sudo -u postgres psql -l docker exec -it psql -U postgres -l # 특정 DB 접속 docker exec -it psql -U -d itemcode_db # 테이블/권한 확인 \dt \dn+ \du \z ``` --- ## expense_db 스키마 / 초기화 DDL: `scripts/sql/expense_db_init.sql` (멱등). DB·역할·테이블·인덱스·트리거를 한 번에 생성. ### 테이블 `expense_items` | 컬럼 | 타입 | 비고 | | --- | --- | --- | | `id` | TEXT PK | 12자 hex (uuid4 앞 12자) | | `owner` | TEXT | 소유자 email (소문자) | | `spent_at` | DATE | 사용일 | | `category` | TEXT | 식대/교통/숙박/비품/접대/통신/기타 | | `method` | TEXT | 법인카드/개인지출/현금 | | `merchant` | TEXT | 가맹점 | | `amount` | BIGINT | 원 단위, ≥ 0 | | `memo` | TEXT | 비고 | | `status` | TEXT | 작성중/제출/승인/반려/정산완료 | | `approver_email` | TEXT | 결재자 email (승인/반려/정산 시 기록) | | `decided_at` | TIMESTAMPTZ | 결재 시점 | | `reject_reason` | TEXT | 반려 사유 | | `created_at` / `updated_at` | TIMESTAMPTZ | 트리거로 자동 갱신 | 인덱스: `(owner, spent_at DESC)`, `(status)`, `(created_at DESC)`. ### 테이블 `expense_attachments` 영수증/기타파일 메타데이터. 실제 파일은 `DATA_DIR/uploads/expense/{item_id}/` 에 저장. | 컬럼 | 타입 | 비고 | | --- | --- | --- | | `id` | TEXT PK | 12자 hex | | `item_id` | TEXT FK | `expense_items(id)` ON DELETE CASCADE | | `owner` | TEXT | 업로드한 사용자 email | | `kind` | TEXT | `receipt` 또는 `other` (CHECK) | | `filename` | TEXT | 원본 파일명 | | `stored_path` | TEXT | 디스크 경로 (절대) | | `content_type` | TEXT | MIME | | `size_bytes` | BIGINT | 바이트 | | `uploaded_at` | TIMESTAMPTZ | 업로드 시각 | 인덱스: `(item_id)`. ### 결재 워크플로 ``` 작성중 ─submit──▶ 제출 ─approve──▶ 승인 ─settle──▶ 정산완료 ▲ │ └─revert/reject──┴─reject──▶ 반려 ─revert──▶ 작성중 ``` - `submit`/`revert`: owner 본인 - `approve`/`reject`/`settle`: `expense_approver` 또는 `admin` - 첨부 추가/항목 수정/삭제: `작성중` 또는 `반려` 상태에서만 ### 마이그레이션 기존 운영 DB 에 신규 컬럼/테이블 적용: ```bash docker exec -i postgres-db psql -U postgres -d expense_db \ < scripts/sql/expense_db_002_workflow_attachments.sql ``` 신규 설치는 `expense_db_init.sql` 하나로 충분 (둘 다 멱등). ### 운영 서버 초기화 (1회) ```bash # 1) 비밀번호 변수 준비 (셸 히스토리에 남지 않게 환경변수 사용) read -s -p "expense_app password: " APP_PWD; echo # 2) PostgreSQL 컨테이너에 DDL 적용 docker exec -i postgres-db psql -U postgres \ -v app_password="$APP_PWD" \ < scripts/sql/expense_db_init.sql # 3) main-app .env 에 EXPENSE_DB_URL 추가 # EXPENSE_DB_URL=postgresql://expense_app:@postgres-db:5432/expense_db # 4) main-app 재기동 docker compose up -d --build ``` ### JSON → DB 마이그레이션 ```bash docker exec -e EXPENSE_DB_URL="$EXPENSE_DB_URL" -it dbx-main \ python scripts/migrate_expense_json_to_db.py \ --json /data/expense.json --dry-run # 결과 확인 후 docker exec -e EXPENSE_DB_URL="$EXPENSE_DB_URL" -it dbx-main \ python scripts/migrate_expense_json_to_db.py --json /data/expense.json ``` > 멱등 INSERT(`ON CONFLICT DO NOTHING`). 원본 JSON 은 건드리지 않는다. --- ## cupang_db 스키마 / 초기화 DDL: `scripts/sql/cupang_db_init.sql` (멱등). DB·역할(`cupang_app`)·테이블·인덱스·트리거·센터 seed 를 한 번에 생성. **JSON 폴백 없음** — `CUPANG_DB_URL` 미설정 시 모듈이 "설정 필요" 안내만 표시. 테이블: | 테이블 | 용도 | | --- | --- | | `cupang_centers` | 입고센터. `active=false` 로 비활성화(사용 중이면 hard delete 금지) | | `cupang_box_rules` | 제품코드별 박스당 입수량(`units_per_box`). `product_code` UNIQUE | | `cupang_shipments` | 출고 묶음 헤더 (작성일/출고일/센터입고일/센터/출고방식/상태/작업자/메모) | | `cupang_shipment_lines` | 출고 라인. `shipment_id` FK ON DELETE CASCADE. `UNIQUE(shipment_id, line_no)` | `status` 허용값: `작성중`, `출고준비`, `출고완료`, `센터입고완료`, `취소`. 삭제는 기본 soft delete(`status='취소'`). 박스 계산은 서버(`store.compute_boxes`)에서 재계산: `required_boxes = ceil(quantity / units_per_box)`. 클라이언트 계산은 미리보기용. 상품은 `cupang_db` 에 복제 저장하지 않는다. 라인에는 `product_code` + `product_name_snapshot` 만 보존(과거 명칭 보존). 상품 검색은 `itemcode_db` **읽기 전용**(`ITEMCODE_DB_URL`, 미설정 시 수동 입력). ### 운영 서버 초기화 (1회, 사용자 승인 후) ```bash read -s -p "cupang_app password: " APP_PWD; echo docker exec -i postgres-db psql -U postgres \ -v app_password="$APP_PWD" \ < scripts/sql/cupang_db_init.sql # main-app .env 에 추가: # CUPANG_DB_URL=postgresql://cupang_app:@postgres-db:5432/cupang_db cd /opt/www/main && docker compose up -d --build ``` > 멱등 스크립트. 기존 DB 가 있으면 DROP 하지 않음. itemcode_db 는 건드리지 않음. --- ## vacation_db 스키마 / 초기화 DDL: `scripts/sql/vacation_db_init.sql` (멱등). DB·역할(`vacation_app`)·테이블·인덱스·트리거·2026 공휴일 seed 를 한 번에 생성. **JSON 폴백 없음** — `VACATION_DB_URL` 미설정 시 모듈이 "설정 필요" 안내만 표시. 테이블: | 테이블 | 용도 | | --- | --- | | `vacation_requests` | 휴가 신청(헤더). 종류/기간/시작·종료 구분(full/am/pm)/일수/사유/상태/승인자/반려사유 | | `vacation_holidays` | 공휴일(`holiday_date` UNIQUE). `is_red=true` 면 달력 빨강 + 일수 계산 제외. 관리자가 settings 에서 추가/수정/삭제 | | `vacation_balances` | 사용자별 연차(`UNIQUE(user_email, year)`). `total_days` 설정, 사용일수는 승인 휴가 합계로 자동 계산 | `status` 허용값: `작성중`, `제출`, `승인`, `반려`, `취소`. 워크플로: 작성중/반려 → 제출 → 승인|반려. 삭제는 기본 soft delete(`status='취소'`). 수정은 작성중/반려 상태에서 본인만. 휴가 일수는 서버(`store.compute_days`)에서 재계산: 주말 + `vacation_holidays(is_red)` 제외, 오전/오후 반차 0.5일, 시작/종료 반차는 각 0.5 차감. 클라이언트 계산은 미리보기(주말만 제외)용. 권한: `vacation`(접근) / `vacation_approver`(승인·반려). admin 은 항상 통과. 공휴일·연차 설정은 admin 전용. ### 운영 서버 초기화 (1회, 사용자 승인 후) ```bash 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 # main-app .env 에 추가: # VACATION_DB_URL=postgresql://vacation_app:@postgres-db:5432/vacation_db cd /opt/www/main && docker compose up -d --build ``` > 멱등 스크립트. 기존 DB 가 있으면 DROP 하지 않음. 공휴일은 연도별로 다르므로 settings 화면에서 추가/수정. --- ## malaysia_stock_db 스키마 / 초기화 DDL: `scripts/sql/malaysia_stock_db_init.sql` (멱등). DB·역할(`malaysia_app`)·테이블·인덱스·트리거·창고/아이템 seed 를 한 번에 생성. **JSON 폴백 없음** — `MALAYSIA_STOCK_DB_URL` 미설정 시 모듈이 "설정 필요" 안내만 표시. 상품명은 `itemcode_db` 읽기 전용 재사용. 테이블: | 테이블 | 용도 | | --- | --- | | `warehouses` | 창고(`warehouse_code` UNIQUE). seed: `MY-WH-01 / Malaysia Warehouse` | | `malaysia_items` | 관리 대상 코드 스코프 + 종류(`individual`/`set`) + 이름 스냅샷. prefix CHECK. seed: 낱개 12 + 세트 5 | | `set_bom` | 세트 구성표. `set_code`(MY-)·`component_code`(MT/MX/MZ) prefix CHECK, `UNIQUE(set_code, component_code)` | | `stock_movement` | 입고/출고/조정/조사 이력. `item_code` 낱개만(CHECK), `movement_type` IN/OUT/ADJUST/STOCKTAKE. `ref_type`·`ref_no`(Shopee/Lazada 연동용) | | `daily_stocktake` | 재고조사 헤더. `status` draft/finalized/cancelled. 같은 날짜+창고 finalized 1건(부분 유니크 인덱스) | | `daily_stocktake_line` | 조사 라인. `sku_code` 낱개+세트 허용(MD- 금지), `qty>=0`, `UNIQUE(stocktake_id, sku_code)` | 현재고 = `SUM(IN) - SUM(OUT) + SUM(ADJUST) + SUM(STOCKTAKE)`. 재고조사 확정 시 시스템 재고와 조사 최종치의 **차이만** STOCKTAKE movement 로 기록. 세트는 movement 불가, 재고조사/BOM 계산 전용. MD- 뚜껑은 전 영역 제외. ### 운영 서버 초기화 (1회, 사용자 승인 후) ```bash read -s -p "malaysia_app password: " APP_PWD; echo docker exec -i postgres-db psql -U postgres \ -v app_password="$APP_PWD" \ < scripts/sql/malaysia_stock_db_init.sql # main-app .env 에 추가: # MALAYSIA_STOCK_DB_URL=postgresql://malaysia_app:@postgres-db:5432/malaysia_stock_db cd /opt/www/main && docker compose up -d --build ``` > 멱등 스크립트. 기존 DB 가 있으면 DROP 하지 않음. itemcode_db 는 건드리지 않음. --- ## 백업 / 복구 (안전 절차) ### 백업 ```bash # 단일 DB 덤프 (운영 권장) docker exec -t \ pg_dump -U -F c -d orderlist_db \ > /var/backups/postgres/orderlist_db_$(date +%F).dump ``` ### 복구 (덮어쓰기 위험 → 사용자 승인 필수) ```bash # 1) 신규 DB로 먼저 복구해 검증 docker exec -i \ pg_restore -U -d < backup.dump # 2) 검증 완료 후 운영 DB 교체 (필요 시) ``` > `pg_restore --clean`은 기존 객체를 삭제한다. **운영 대상 DB에서 절대 무단 실행 금지**. --- ## .env 관련 - DB 접속 정보(`*_HOST`, `*_PORT`, `*_USER`, `*_PASSWORD`, `*_NAME`)는 모두 `.env`로 관리한다. - `.env`는 **Git에 올리지 않는다**. `.env.example`만 커밋한다. - 비밀값 유출이 의심되면 즉시 회전(비밀번호/키 변경)을 진행한다.