증상: 상세페이지를 적용해도 편집기에 수정 전 소스가 보이고 한참 뒤에야 반영됨. 원인은 우리 캐시가 아니라(전부 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>
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 작업 원칙
- 작업 전 백업 우선. 백업 없는 변경은 진행하지 않는다.
- 테이블 owner, 권한, sequence 권한을 확인한다.
- 운영 DB의
DROP,TRUNCATE, 조건 없는 대량DELETE/UPDATE는 사용자 확인 없이 실행 금지. - 스키마 변경은 마이그레이션 스크립트(
scripts/또는 alembic 등)로 관리한다. - 운영 DB와 개발 DB의 접속 정보를 혼동하지 않는다 (
.env로 분리).
위험 명령 (사용자 승인 필수)
| 명령 | 비고 |
|---|---|
DROP DATABASE |
복구 불가. 백업 없으면 절대 실행 금지 |
DROP TABLE / DROP SCHEMA |
의존 객체 확인 필수 |
TRUNCATE |
FK CASCADE 시 광범위 삭제 위험 |
조건 없는 DELETE / UPDATE |
WHERE 없는 문 차단 |
docker volume rm <postgres_volume> |
운영 데이터 영구 손실 |
docker compose down -v |
볼륨까지 제거. 운영에서 금지 |
실행 전 반드시:
- 백업 확인 (
pg_dump, 컨테이너 외부 마운트) - 영향 범위 설명
- 사용자 명시 승인
자주 쓰는 점검 명령
# 컨테이너/네트워크
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 을 다시 조회해
BACKUPrevision 으로 저장한다. 로컬 DB 의 마지막 값을 현재값으로 가정하지 않는다. - 예약의
restore_revision_id는 예약 실행 순간 만든 BACKUP 을 가리킨다(예약 생성 시점 값이 아님). - 일괄 예약은 상품 1건당 1행 + 공통
parent_job_id— 한 상품 실패가 나머지를 막지 않는다. - 토큰 암호화 키는
.env의CAFE24_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로 관리한다. .env는 Git에 올리지 않는다..env.example만 커밋한다.- 비밀값 유출이 의심되면 즉시 회전(비밀번호/키 변경)을 진행한다.