Files
dbx-main/docs/DATABASES.md
king 40766d805d feat(cafe24): 읽기 지연 보정("마지막 쓰기가 권위") + 상품 정보 패널
증상: 상세페이지를 적용해도 편집기에 수정 전 소스가 보이고 한참 뒤에야
반영됨. 원인은 우리 캐시가 아니라(전부 no-store) 카페24 관리자 API 가 PUT
뒤 한동안 GET 에서 예전 값을 돌려주는 읽기 지연. 예전 코드는 2.4초만
기다린 뒤 GET 값을 그대로 믿어 예전 소스 표시·지문 충돌 오판·예전 값
백업이 생겼다.

- 상세설명: 쓰기 성공 시 MANUAL/SCHEDULED revision 을 기준으로, 카페24 값이
  유예시간 안의 revision 중 하나와 같으면 지연(pending)으로 보고 마지막
  쓰기를 표시·지문 기준으로 쓴다. 모르는 값이면 외부 변경(external).
  store.resolve_description / db.revision_digests(md5) / 배너 2종.
- 적용(apply)은 유효 현재값으로 BACKUP·지문 대조·변경없음 판정. 재조회
  확인 결과는 감사로그에만 남긴다.
- 스칼라(상품명·가격·이미지·진열/판매): PUT 응답을 cafe24_products.
  last_write_snapshot(JSONB, 마이그레이션 004)에 남기고 GET 의 updated_date
  가 그보다 이전이면 스냅샷으로 덮어씀. 옵션/품목도 섹션별 스냅샷.
- 3분할 화면: 목록 | 편집기 | 상품 정보 패널(_side.html, /pane 이 두 조각을
  한 응답으로). routes_product_info.py JSON API — 상품명/판매가/공급가/
  소비자가, 대표이미지 업로드(POST /admin/products/images → PUT detail_image
  + image_upload_type=A), 옵션 생성/이름·썸네일·표시방식 수정/삭제, 품목
  자체코드·추가금액·진열·판매 일괄 수정. 화면은 PUT 응답으로 그린다.
- client.delete/timeout, products.upload_images·options·variants 래퍼.
- 유닛테스트 21건 추가(88 통과), 문서(CAFE24_MODULE 3-3/3-4, DATABASES,
  .env.example CAFE24_READ_LAG_GRACE_MIN) 갱신.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:07:42 +09:00

16 KiB

Databases

PostgreSQL DB 목록

DB명 용도
itemcode_db 상품코드, 단품/세트 구성, 채널 ↔ 사내 코드 매칭
orderlist_db 주문 수집·분석·관리 (구 orderlist_app)
return_db 반품·교환·CS 데이터
expense_db 개인경비 / 법인카드 사용내역 / 정산
cupang_db 쿠팡 밀크런 출고 묶음 / 출고 라인 / 입고센터 / 박스 입수량 규칙
malaysia_stock_db 말레이시아 창고 재고관리 — 창고/아이템/세트 BOM/입출고 이력/일일 재고조사
cafe24_db 카페24 연동 — OAuth 토큰/상품 캐시/상세페이지 버전/예약/감사·API 로그

명명 규칙

  • 모든 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 <postgres_volume> 운영 데이터 영구 손실
docker compose down -v 볼륨까지 제거. 운영에서 금지

실행 전 반드시:

  1. 백업 확인 (pg_dump, 컨테이너 외부 마운트)
  2. 영향 범위 설명
  3. 사용자 명시 승인

자주 쓰는 점검 명령

# 컨테이너/네트워크
docker ps
docker network ls
docker inspect <postgres_container_name>

# DB 목록 / 접속
sudo -u postgres psql -l
docker exec -it <postgres_container_name> psql -U postgres -l

# 특정 DB 접속
docker exec -it <postgres_container_name> psql -U <user> -d itemcode_db

# 테이블/권한 확인
\dt
\dn+
\du
\z <table_name>

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 에 신규 컬럼/테이블 적용:

docker exec -i postgres-db psql -U postgres -d expense_db \
  < scripts/sql/expense_db_002_workflow_attachments.sql

신규 설치는 expense_db_init.sql 하나로 충분 (둘 다 멱등).

운영 서버 초기화 (1회)

# 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:<APP_PWD>@postgres-db:5432/expense_db

# 4) main-app 재기동
docker compose up -d --build

JSON → DB 마이그레이션

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)
cupang_box_calc_drafts 박스 계산 화면 임시 저장. title + payload(JSONB: 입력 품목·열어둔 센터·분배 내역). 같은 제목이면 덮어씀

status 허용값: 작성중, 출고준비, 출고완료, 센터입고완료, 취소. 삭제는 기본 soft delete(status='취소').

박스 계산은 서버(store.compute_boxes)에서 재계산: required_boxes = ceil(quantity / units_per_box). 클라이언트 계산은 미리보기용.

임시 저장(cupang_box_calc_drafts)은 화면 상태 스냅샷만 담는다. 불러올 때 박스 수는 저장값을 쓰지 않고 서버에서 다시 계산하고, 그 위에 분배 내역을 얹는다(사라진 항목은 제외). 마이그레이션: scripts/sql/cupang_db_003_box_calc_drafts.sql.

상품은 cupang_db 에 복제 저장하지 않는다. 라인에는 product_code + product_name_snapshot 만 보존(과거 명칭 보존). 상품 검색은 itemcode_db 읽기 전용(ITEMCODE_DB_URL, 미설정 시 수동 입력).

운영 서버 초기화 (1회, 사용자 승인 후)

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:<APP_PWD>@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회, 사용자 승인 후)

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:<APP_PWD>@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회, 사용자 승인 후)

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:<APP_PWD>@postgres-db:5432/malaysia_stock_db
cd /opt/www/main && docker compose up -d --build

멱등 스크립트. 기존 DB 가 있으면 DROP 하지 않음. itemcode_db 는 건드리지 않음.


cafe24_db 스키마 / 초기화

DDL: scripts/sql/cafe24_db_init.sql (멱등). DB·역할(cafe24_app)·테이블·인덱스·트리거를 한 번에 생성. JSON 폴백 없음CAFE24_DB_URL 미설정 시 모듈이 "설정 필요" 안내만 표시.

테이블:

테이블 용도
cafe24_oauth_tokens 쇼핑몰별 OAuth 토큰(mall_id UNIQUE). access/refresh 는 Fernet 암호문으로 저장. 상품관리 + 향후 주문관리가 공유
cafe24_products 상품 캐시(product_no UNIQUE). 목록/검색 속도용이며 source of truth 는 언제나 카페24. last_write_snapshot(JSONB, 마이그레이션 004) 에 카페24 PUT 응답을 섹션별(product/options/variants)로 남겨 읽기 지연(PUT 뒤 GET 이 한동안 예전 값을 돌려줌) 동안 화면 기준으로 쓴다
cafe24_product_revisions 상세페이지 HTML 버전(append-only). revision_type SYNC/DRAFT/BACKUP/MANUAL/SCHEDULED/ROLLBACK
cafe24_product_schedules 예약 작업. status PENDING/PROCESSING/SUCCESS/FAILED/CANCELLED, 재시도·자동종료·복원 대상 포함
cafe24_audit_logs 누가 무엇을 바꿨나. worker 수행분은 actor='SCHEDULER'
cafe24_api_logs 카페24 API 호출 기록. 토큰/Authorization/client_secret 미기록

핵심 규칙:

  • 카페24에 쓰기 직전 반드시 현재 HTML 을 다시 조회해 BACKUP revision 으로 저장한다. 로컬 DB 의 마지막 값을 현재값으로 가정하지 않는다.
  • 예약의 restore_revision_id예약 실행 순간 만든 BACKUP 을 가리킨다(예약 생성 시점 값이 아님).
  • 일괄 예약은 상품 1건당 1행 + 공통 parent_job_id — 한 상품 실패가 나머지를 막지 않는다.
  • 토큰 암호화 키는 .envCAFE24_TOKEN_SECRET. 값을 바꾸면 기존 토큰을 복호화할 수 없어 카페24 재연결이 필요하다.
  • 기존 운영 DB 에는 마이그레이션 002·003·004 를 순서대로 적용한다(전부 멱등): docker exec -i postgres-db psql -U postgres -d cafe24_db < scripts/sql/cafe24_db_004_write_snapshot.sql

운영 서버 초기화 (1회, 사용자 승인 후)

read -s -p "cafe24_app password: " APP_PWD; echo
docker exec -i postgres-db psql -U postgres \
  -v app_password="$APP_PWD" \
  < scripts/sql/cafe24_db_init.sql
# main-app .env 에 추가:
#   CAFE24_DB_URL=postgresql://cafe24_app:<APP_PWD>@postgres-db:5432/cafe24_db
#   CAFE24_MALL_ID / CAFE24_CLIENT_ID / CAFE24_CLIENT_SECRET / CAFE24_REDIRECT_URI
#   CAFE24_TOKEN_SECRET=$(openssl rand -hex 32)
cd /opt/www/main && docker compose up -d --build web

멱등 스크립트. 기존 DB 가 있으면 DROP 하지 않음. 상세는 docs/CAFE24_MODULE.md.


백업 / 복구 (안전 절차)

백업

# 단일 DB 덤프 (운영 권장)
docker exec -t <postgres_container_name> \
  pg_dump -U <user> -F c -d orderlist_db \
  > /var/backups/postgres/orderlist_db_$(date +%F).dump

복구 (덮어쓰기 위험 → 사용자 승인 필수)

# 1) 신규 DB로 먼저 복구해 검증
docker exec -i <postgres_container_name> \
  pg_restore -U <user> -d <new_db_name> < backup.dump

# 2) 검증 완료 후 운영 DB 교체 (필요 시)

pg_restore --clean은 기존 객체를 삭제한다. 운영 대상 DB에서 절대 무단 실행 금지.


.env 관련

  • DB 접속 정보(*_HOST, *_PORT, *_USER, *_PASSWORD, *_NAME)는 모두 .env로 관리한다.
  • .envGit에 올리지 않는다. .env.example만 커밋한다.
  • 비밀값 유출이 의심되면 즉시 회전(비밀번호/키 변경)을 진행한다.