-- ===================================================================== -- dispatch_db 초기화 스크립트 (PostgreSQL) — 말레이시아 TikTok 출고관리 -- ===================================================================== -- 멱등(idempotent): 여러 번 실행해도 안전. 기존 데이터를 삭제하지 않는다. -- -- ⚠️ 실행 전 사용자 승인 + 백업 확인 필수. DROP/TRUNCATE 없음. -- -- 실행 방법 (운영 PostgreSQL 컨테이너명: postgres-db): -- -- 1) DB / 역할 / 스키마 생성 (superuser 로 1회) -- read -s -p "dispatch_app password: " APP_PWD; echo -- docker exec -i postgres-db psql -U postgres \ -- -v app_password="$APP_PWD" \ -- < scripts/sql/dispatch_db_init.sql -- -- 2) main-app .env 에 연결 정보 등록 -- DISPATCH_DB_URL=postgresql://dispatch_app:@postgres-db:5432/dispatch_db -- -- 3) main-app 재기동 -- cd /opt/www/main && docker compose up -d --build web -- -- 주의: -- - 기존 DB 가 있으면 DROP 하지 않는다(CREATE DATABASE 는 미존재 시에만). -- - 비밀번호는 절대 Git 에 커밋하지 않는다. psql -v 로만 전달. -- - 받는 사람 정보(이름/전화/주소)는 출고 작업 카드 표시 + 출고 엑셀 생성을 -- 위해 박스 단위로 보관한다(개인정보 — 접근/보관 정책 검토 대상). -- - 작업 기준 키: order_id / package_id / tracking_id / seller_sku / quantity. -- ===================================================================== \set ON_ERROR_STOP on -- DB 가 없을 때만 생성 SELECT 'CREATE DATABASE dispatch_db ENCODING ''UTF8'' TEMPLATE template0' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'dispatch_db') \gexec -- 앱 전용 로그인 역할 SELECT 'CREATE ROLE dispatch_app LOGIN PASSWORD ' || quote_literal(:'app_password') WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dispatch_app') \gexec -- 항상 최신 비밀번호로 동기화 SELECT 'ALTER ROLE dispatch_app WITH LOGIN PASSWORD ' || quote_literal(:'app_password') \gexec GRANT CONNECT ON DATABASE dispatch_db TO dispatch_app; -- dispatch_db 컨텍스트로 전환 \connect dispatch_db -- ── updated_at 자동 갱신 트리거 함수 (멱등: CREATE OR REPLACE) ── CREATE OR REPLACE FUNCTION dispatch_set_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at := now(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- ════════════════════════════════════════════════════════════ -- 1) 출고 배치 (하루 1회~여러 회 업로드 단위) -- 같은 날짜/플랫폼이라도 batch_name 으로 구분해 여러 배치 생성 가능. -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS dispatch_batches ( id BIGSERIAL PRIMARY KEY, platform TEXT NOT NULL DEFAULT 'TikTok', dispatch_date DATE NOT NULL, batch_name TEXT NOT NULL DEFAULT '', picking_pdf_path TEXT NOT NULL DEFAULT '', label_pdf_path TEXT NOT NULL DEFAULT '', order_export_path TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_dispatch_batches_date ON dispatch_batches (dispatch_date DESC); DROP TRIGGER IF EXISTS trg_dispatch_batches_updated ON dispatch_batches; CREATE TRIGGER trg_dispatch_batches_updated BEFORE UPDATE ON dispatch_batches FOR EACH ROW EXECUTE FUNCTION dispatch_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 2) 출고 박스 (1박스 = 1행 = 1작업카드) -- 묶음 기준: package_id > tracking_id > order_id (파서가 결정). -- 받는 사람(recipient_*) 정보 보관 — 작업 카드 표시 + 출고 엑셀 생성용. -- seq 는 배치 내 박스 표시 순번(No). -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS dispatch_parcels ( id BIGSERIAL PRIMARY KEY, batch_id BIGINT NOT NULL REFERENCES dispatch_batches(id) ON DELETE CASCADE, seq INTEGER NOT NULL DEFAULT 0, order_id TEXT NOT NULL DEFAULT '', package_id TEXT NOT NULL DEFAULT '', tracking_id TEXT NOT NULL DEFAULT '', shipping_provider TEXT NOT NULL DEFAULT '', recipient_name TEXT NOT NULL DEFAULT '', recipient_phone TEXT NOT NULL DEFAULT '', recipient_address TEXT NOT NULL DEFAULT '', product_ready BOOLEAN NOT NULL DEFAULT FALSE, packed BOOLEAN NOT NULL DEFAULT FALSE, label_attached BOOLEAN NOT NULL DEFAULT FALSE, handed_to_kagayaku BOOLEAN NOT NULL DEFAULT FALSE, courier_scanned BOOLEAN NOT NULL DEFAULT FALSE, memo TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_dispatch_parcels_batch ON dispatch_parcels (batch_id, seq); CREATE INDEX IF NOT EXISTS idx_dispatch_parcels_order ON dispatch_parcels (order_id); CREATE INDEX IF NOT EXISTS idx_dispatch_parcels_tracking ON dispatch_parcels (tracking_id); CREATE INDEX IF NOT EXISTS idx_dispatch_parcels_package ON dispatch_parcels (package_id); DROP TRIGGER IF EXISTS trg_dispatch_parcels_updated ON dispatch_parcels; CREATE TRIGGER trg_dispatch_parcels_updated BEFORE UPDATE ON dispatch_parcels FOR EACH ROW EXECUTE FUNCTION dispatch_set_updated_at(); -- ════════════════════════════════════════════════════════════ -- 3) 박스 안 상품 (1박스 N상품, 같은 SKU 는 수량 합산되어 1행) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS dispatch_items ( id BIGSERIAL PRIMARY KEY, parcel_id BIGINT NOT NULL REFERENCES dispatch_parcels(id) ON DELETE CASCADE, seller_sku TEXT NOT NULL DEFAULT '', product_name TEXT NOT NULL DEFAULT '', quantity INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_dispatch_items_parcel ON dispatch_items (parcel_id); CREATE INDEX IF NOT EXISTS idx_dispatch_items_sku ON dispatch_items (seller_sku); -- ════════════════════════════════════════════════════════════ -- 4) 작업 상태 변경 로그 (감사/추적용) -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS dispatch_logs ( id BIGSERIAL PRIMARY KEY, parcel_id BIGINT NOT NULL REFERENCES dispatch_parcels(id) ON DELETE CASCADE, action TEXT NOT NULL DEFAULT '', old_value TEXT NOT NULL DEFAULT '', new_value TEXT NOT NULL DEFAULT '', worker_name TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_dispatch_logs_parcel ON dispatch_logs (parcel_id); -- ════════════════════════════════════════════════════════════ -- 5) 말레이시아 재고 차감 1회성 기록 (날짜 단위 멱등 가드) -- 그날 출고를 말레이시아 재고관리(OUT)로 1회만 반영. 존재 시 재차감 차단. -- ════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS dispatch_stock_deductions ( dispatch_date DATE PRIMARY KEY, warehouse_code TEXT NOT NULL DEFAULT '', single_lines INTEGER NOT NULL DEFAULT 0, combo_lines INTEGER NOT NULL DEFAULT 0, worker_name TEXT NOT NULL DEFAULT '', deducted_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ════════════════════════════════════════════════════════════ -- 6) 권한 (dispatch_app: CRUD only) -- ════════════════════════════════════════════════════════════ GRANT USAGE ON SCHEMA public TO dispatch_app; GRANT SELECT, INSERT, UPDATE, DELETE ON dispatch_batches, dispatch_parcels, dispatch_items, dispatch_logs, dispatch_stock_deductions TO dispatch_app; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO dispatch_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dispatch_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO dispatch_app; SELECT 'dispatch_db ready' AS status;