Files
dbx-main/scripts/sql/cafe24_db_init.sql
T
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

242 lines
13 KiB
PL/PgSQL

-- =====================================================================
-- cafe24_db 초기화 스크립트 (PostgreSQL) — 카페24 상품 상세페이지 관리
-- =====================================================================
-- 멱등(idempotent): 여러 번 실행해도 안전. 기존 데이터를 삭제하지 않는다.
--
-- ⚠️ 실행 전 사용자 승인 + 백업 확인 필수. DROP/TRUNCATE 없음.
--
-- 실행 방법 (운영 PostgreSQL 컨테이너명: postgres-db):
--
-- 1) DB / 역할 / 스키마 생성 (superuser 로 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
--
-- 2) main-app .env 에 연결 정보 등록
-- CAFE24_DB_URL=postgresql://cafe24_app:<APP_PWD>@postgres-db:5432/cafe24_db
--
-- 3) main-app 재기동
-- cd /opt/www/main && docker compose up -d --build web
--
-- 주의:
-- - 기존 DB 가 있으면 DROP 하지 않는다(CREATE DATABASE 는 미존재 시에만).
-- - 비밀번호는 절대 Git 에 커밋하지 않는다. psql -v 로만 전달.
-- - OAuth 토큰(access/refresh)은 애플리케이션에서 Fernet 으로 암호화한 뒤
-- 저장한다. 키는 .env 의 CAFE24_TOKEN_SECRET. 이 DB 에 평문 토큰은 없다.
-- - cafe24_api_logs 에는 Authorization 헤더/토큰/client_secret 을 절대
-- 기록하지 않는다(엔드포인트·상태코드·소요시간·오류메시지만).
-- - 향후 카페24 주문관리 모듈도 이 DB(특히 cafe24_oauth_tokens)를 재사용한다.
-- =====================================================================
\set ON_ERROR_STOP on
-- DB 가 없을 때만 생성
SELECT 'CREATE DATABASE cafe24_db ENCODING ''UTF8'' TEMPLATE template0'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'cafe24_db')
\gexec
-- 앱 전용 로그인 역할
SELECT 'CREATE ROLE cafe24_app LOGIN PASSWORD ' || quote_literal(:'app_password')
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cafe24_app')
\gexec
-- 항상 최신 비밀번호로 동기화
SELECT 'ALTER ROLE cafe24_app WITH LOGIN PASSWORD ' || quote_literal(:'app_password')
\gexec
GRANT CONNECT ON DATABASE cafe24_db TO cafe24_app;
-- cafe24_db 컨텍스트로 전환
\connect cafe24_db
-- ── updated_at 자동 갱신 트리거 함수 (멱등: CREATE OR REPLACE) ──
CREATE OR REPLACE FUNCTION cafe24_set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- ════════════════════════════════════════════════════════════
-- 1) OAuth 토큰 (쇼핑몰 1개당 1행)
-- access_token / refresh_token 은 Fernet 암호문(TEXT)으로 저장한다.
-- 상품관리 + 향후 주문관리가 공유한다.
-- ════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cafe24_oauth_tokens (
id BIGSERIAL PRIMARY KEY,
mall_id TEXT NOT NULL UNIQUE,
access_token TEXT NOT NULL DEFAULT '',
refresh_token TEXT NOT NULL DEFAULT '',
access_token_expires_at TIMESTAMPTZ,
refresh_token_expires_at TIMESTAMPTZ,
scopes TEXT NOT NULL DEFAULT '',
last_refreshed_at TIMESTAMPTZ,
last_error TEXT NOT NULL DEFAULT '',
connected_by TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
DROP TRIGGER IF EXISTS trg_cafe24_oauth_tokens_updated ON cafe24_oauth_tokens;
CREATE TRIGGER trg_cafe24_oauth_tokens_updated
BEFORE UPDATE ON cafe24_oauth_tokens
FOR EACH ROW EXECUTE FUNCTION cafe24_set_updated_at();
-- ════════════════════════════════════════════════════════════
-- 2) 상품 캐시 (source of truth 는 언제나 Cafe24. 목록/검색 속도용)
-- ════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cafe24_products (
id BIGSERIAL PRIMARY KEY,
product_no BIGINT NOT NULL UNIQUE,
product_code TEXT NOT NULL DEFAULT '',
product_name TEXT NOT NULL DEFAULT '',
display BOOLEAN NOT NULL DEFAULT TRUE,
selling BOOLEAN NOT NULL DEFAULT TRUE,
last_synced_at TIMESTAMPTZ,
-- 쓰기 직후 스냅샷(카페24 읽기 지연 보정, 마이그레이션 004)
last_write_snapshot JSONB,
last_written_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE cafe24_products
ADD COLUMN IF NOT EXISTS last_write_snapshot JSONB,
ADD COLUMN IF NOT EXISTS last_written_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_cafe24_products_name ON cafe24_products (product_name);
CREATE INDEX IF NOT EXISTS idx_cafe24_products_code ON cafe24_products (product_code);
DROP TRIGGER IF EXISTS trg_cafe24_products_updated ON cafe24_products;
CREATE TRIGGER trg_cafe24_products_updated
BEFORE UPDATE ON cafe24_products
FOR EACH ROW EXECUTE FUNCTION cafe24_set_updated_at();
-- ════════════════════════════════════════════════════════════
-- 3) 상세페이지 HTML 버전 (append-only — UPDATE/DELETE 하지 않는다)
-- revision_type:
-- SYNC Cafe24 에서 읽어온 현재값 스냅샷
-- DRAFT 저장만 한 초안(미적용)
-- BACKUP Cafe24 에 쓰기 직전 자동 백업 ← 복원 기준
-- MANUAL 즉시 적용한 내용
-- SCHEDULED 예약으로 적용한 내용
-- ROLLBACK 과거 버전을 되돌린 내용
-- ════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cafe24_product_revisions (
id BIGSERIAL PRIMARY KEY,
product_no BIGINT NOT NULL,
html_content TEXT NOT NULL DEFAULT '',
revision_type TEXT NOT NULL DEFAULT 'DRAFT',
memo TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT chk_cafe24_revision_type CHECK (
revision_type IN ('SYNC','DRAFT','BACKUP','MANUAL','SCHEDULED','ROLLBACK')
)
);
CREATE INDEX IF NOT EXISTS idx_cafe24_revisions_product
ON cafe24_product_revisions (product_no, created_at DESC, id DESC);
-- ════════════════════════════════════════════════════════════
-- 4) 예약 작업
-- 한 상품 = 한 행. 일괄 예약은 parent_job_id 로 묶되 행은 개별이므로
-- 한 상품 실패가 나머지를 막지 않는다.
-- end_at/end_action: 프로모션 종료 후 자동 복원용.
-- restore = 적용 직전 BACKUP(restore_revision_id)으로 되돌림
-- revision = end_revision_id 를 적용
-- restore_revision_id 는 예약 "실행 순간" Cafe24 에서 다시 읽어 만든
-- BACKUP revision 을 가리킨다(예약 생성 시점 값이 아님).
-- ════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cafe24_product_schedules (
id BIGSERIAL PRIMARY KEY,
product_no BIGINT NOT NULL,
revision_id BIGINT REFERENCES cafe24_product_revisions(id) ON DELETE RESTRICT,
scheduled_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
retry_count INTEGER NOT NULL DEFAULT 0,
next_retry_at TIMESTAMPTZ,
last_error TEXT NOT NULL DEFAULT '',
restore_revision_id BIGINT REFERENCES cafe24_product_revisions(id) ON DELETE SET NULL,
end_at TIMESTAMPTZ,
end_action TEXT NOT NULL DEFAULT '',
end_revision_id BIGINT REFERENCES cafe24_product_revisions(id) ON DELETE SET NULL,
parent_job_id TEXT NOT NULL DEFAULT '',
memo TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
CONSTRAINT chk_cafe24_schedule_status CHECK (
status IN ('PENDING','PROCESSING','SUCCESS','FAILED','CANCELLED')
),
CONSTRAINT chk_cafe24_schedule_end_action CHECK (
end_action IN ('','restore','revision')
)
);
-- worker 의 due 조회 인덱스 (PENDING + 시간순)
CREATE INDEX IF NOT EXISTS idx_cafe24_schedules_due
ON cafe24_product_schedules (scheduled_at)
WHERE status = 'PENDING';
CREATE INDEX IF NOT EXISTS idx_cafe24_schedules_product
ON cafe24_product_schedules (product_no, scheduled_at DESC);
CREATE INDEX IF NOT EXISTS idx_cafe24_schedules_parent
ON cafe24_product_schedules (parent_job_id);
DROP TRIGGER IF EXISTS trg_cafe24_schedules_updated ON cafe24_product_schedules;
CREATE TRIGGER trg_cafe24_schedules_updated
BEFORE UPDATE ON cafe24_product_schedules
FOR EACH ROW EXECUTE FUNCTION cafe24_set_updated_at();
-- ════════════════════════════════════════════════════════════
-- 5) 작업 감사 로그 (누가 무엇을 바꿨나)
-- worker 가 수행한 작업은 actor='SCHEDULER'.
-- ════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cafe24_audit_logs (
id BIGSERIAL PRIMARY KEY,
actor TEXT NOT NULL DEFAULT '',
action TEXT NOT NULL DEFAULT '',
product_no BIGINT,
revision_id BIGINT,
schedule_id BIGINT,
result TEXT NOT NULL DEFAULT '',
detail TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_cafe24_audit_created ON cafe24_audit_logs (created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_cafe24_audit_product ON cafe24_audit_logs (product_no, created_at DESC);
-- ════════════════════════════════════════════════════════════
-- 6) Cafe24 API 호출 로그 (실패 분석용 최소 정보)
-- ⚠️ Authorization 헤더 / access_token / refresh_token / client_secret 은
-- 절대 저장하지 않는다.
-- ════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cafe24_api_logs (
id BIGSERIAL PRIMARY KEY,
endpoint TEXT NOT NULL DEFAULT '',
method TEXT NOT NULL DEFAULT '',
product_no BIGINT,
http_status INTEGER,
result TEXT NOT NULL DEFAULT '',
error_message TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_cafe24_api_logs_created ON cafe24_api_logs (created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_cafe24_api_logs_status ON cafe24_api_logs (http_status);
-- ════════════════════════════════════════════════════════════
-- 7) 권한 (cafe24_app: CRUD only, DDL 없음)
-- ════════════════════════════════════════════════════════════
GRANT USAGE ON SCHEMA public TO cafe24_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON
cafe24_oauth_tokens, cafe24_products, cafe24_product_revisions,
cafe24_product_schedules, cafe24_audit_logs, cafe24_api_logs
TO cafe24_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO cafe24_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO cafe24_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO cafe24_app;
SELECT 'cafe24_db ready' AS status;