5c43cb801c
뚜껑 저장 시 daily_stocktake_rack.sku_code CHECK 제약(MT/MX/MZ/MY만) 위반으로 Internal Server Error 발생. init SQL 의 CHECK 에 'MD-%' 추가, 운영 DB용 멱등 마이그레이션(malaysia_stock_db_rack_allow_lid.sql) 제공. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
318 lines
18 KiB
PL/PgSQL
318 lines
18 KiB
PL/PgSQL
-- =====================================================================
|
|
-- malaysia_stock_db 초기화 스크립트 (PostgreSQL) — 말레이시아 창고 재고관리
|
|
-- =====================================================================
|
|
-- 멱등(idempotent): 여러 번 실행해도 안전. 기존 데이터를 삭제하지 않는다.
|
|
--
|
|
-- ⚠️ 실행 전 사용자 승인 + 백업 확인 필수. DROP/TRUNCATE 없음.
|
|
--
|
|
-- 실행 방법 (운영 PostgreSQL 컨테이너명: postgres-db):
|
|
--
|
|
-- 1) DB / 역할 / 스키마 생성 (superuser 로 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
|
|
--
|
|
-- 2) main-app .env 에 연결 정보 등록
|
|
-- MALAYSIA_STOCK_DB_URL=postgresql://malaysia_app:<APP_PWD>@postgres-db:5432/malaysia_stock_db
|
|
--
|
|
-- 3) main-app 재기동
|
|
-- cd /opt/www/main && docker compose up -d --build
|
|
--
|
|
-- 주의:
|
|
-- - 기존 DB 가 있으면 DROP 하지 않는다(CREATE DATABASE 는 미존재 시에만).
|
|
-- - 비밀번호는 절대 Git 에 커밋하지 않는다. psql -v 로만 전달.
|
|
-- - itemcode_db 는 이 스크립트가 건드리지 않는다(상품명은 읽기 전용 참조).
|
|
-- - 재고 계산 원칙:
|
|
-- 현재고 = SUM(IN) - SUM(OUT) + SUM(ADJUST 부호) + SUM(STOCKTAKE 부호)
|
|
-- ADJUST/STOCKTAKE 는 음수 qty 허용(±). IN/OUT 은 양수만.
|
|
-- =====================================================================
|
|
|
|
\set ON_ERROR_STOP on
|
|
|
|
-- DB 가 없을 때만 생성
|
|
SELECT 'CREATE DATABASE malaysia_stock_db ENCODING ''UTF8'' TEMPLATE template0'
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'malaysia_stock_db')
|
|
\gexec
|
|
|
|
-- 앱 전용 로그인 역할 (cupang_app 패턴과 동일)
|
|
SELECT 'CREATE ROLE malaysia_app LOGIN PASSWORD ' || quote_literal(:'app_password')
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'malaysia_app')
|
|
\gexec
|
|
|
|
-- 항상 최신 비밀번호로 동기화
|
|
SELECT 'ALTER ROLE malaysia_app WITH LOGIN PASSWORD ' || quote_literal(:'app_password')
|
|
\gexec
|
|
|
|
GRANT CONNECT ON DATABASE malaysia_stock_db TO malaysia_app;
|
|
|
|
-- malaysia_stock_db 컨텍스트로 전환
|
|
\connect malaysia_stock_db
|
|
|
|
-- ── updated_at 자동 갱신 트리거 함수 (멱등: CREATE OR REPLACE) ──
|
|
CREATE OR REPLACE FUNCTION malaysia_set_updated_at() RETURNS trigger AS $$
|
|
BEGIN
|
|
NEW.updated_at := now();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 1) 창고
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS warehouses (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
warehouse_code TEXT NOT NULL UNIQUE,
|
|
warehouse_name TEXT NOT NULL,
|
|
country TEXT NOT NULL DEFAULT 'MY',
|
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
DROP TRIGGER IF EXISTS trg_warehouses_updated ON warehouses;
|
|
CREATE TRIGGER trg_warehouses_updated
|
|
BEFORE UPDATE ON warehouses
|
|
FOR EACH ROW EXECUTE FUNCTION malaysia_set_updated_at();
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 2) 재고관리 대상 아이템(스코프 카탈로그)
|
|
-- 상품 마스터를 복제하는 것이 아니라, "말레이시아 창고에서 관리하는
|
|
-- 코드 목록과 종류(낱개/세트)"만 보관한다. item_name 은 스냅샷이며
|
|
-- itemcode_db(읽기 전용)에서 최신 명칭을 덮어쓸 수 있다.
|
|
-- kind: 'individual'(MT/MX/MZ) | 'set'(MY). MD- 뚜껑은 제외(미등록).
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS malaysia_items (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
item_code TEXT NOT NULL UNIQUE,
|
|
item_name TEXT NOT NULL DEFAULT '',
|
|
kind TEXT NOT NULL CHECK (kind IN ('individual', 'set')),
|
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- prefix 규칙: 낱개는 MT-/MX-/MZ-, 세트는 MY-. MD- 금지.
|
|
CHECK (
|
|
(kind = 'individual' AND (item_code LIKE 'MT-%' OR item_code LIKE 'MX-%' OR item_code LIKE 'MZ-%'))
|
|
OR
|
|
(kind = 'set' AND item_code LIKE 'MY-%')
|
|
)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_malaysia_items_kind ON malaysia_items (kind);
|
|
|
|
DROP TRIGGER IF EXISTS trg_malaysia_items_updated ON malaysia_items;
|
|
CREATE TRIGGER trg_malaysia_items_updated
|
|
BEFORE UPDATE ON malaysia_items
|
|
FOR EACH ROW EXECUTE FUNCTION malaysia_set_updated_at();
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 3) 세트 구성표 (BOM)
|
|
-- set_code(MY-) 안에 component_code(MT-/MX-/MZ-) 가 몇 개 들어가는지.
|
|
-- MY- 가 MY- 를 포함하는 중첩은 불가(component 는 MY- 금지). MD- 금지.
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS set_bom (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
set_code TEXT NOT NULL CHECK (set_code LIKE 'MY-%'),
|
|
component_code TEXT NOT NULL CHECK (
|
|
component_code LIKE 'MT-%'
|
|
OR component_code LIKE 'MX-%'
|
|
OR component_code LIKE 'MZ-%'),
|
|
component_qty INTEGER NOT NULL CHECK (component_qty > 0),
|
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (set_code, component_code)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_set_bom_set ON set_bom (set_code);
|
|
|
|
DROP TRIGGER IF EXISTS trg_set_bom_updated ON set_bom;
|
|
CREATE TRIGGER trg_set_bom_updated
|
|
BEFORE UPDATE ON set_bom
|
|
FOR EACH ROW EXECUTE FUNCTION malaysia_set_updated_at();
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 4) 재고 이동 이력 (입고/출고/조정/재고조사 반영)
|
|
-- item_code 는 낱개(MT-/MX-/MZ-)만 저장한다. MY-/MD- 는 movement 금지.
|
|
-- qty 부호 규칙:
|
|
-- IN/OUT → 양수만(방향은 movement_type 으로 구분)
|
|
-- ADJUST → 음수 허용(±)
|
|
-- STOCKTAKE → 음수 허용(±, 재고조사 차이 반영분)
|
|
-- 현재고 = SUM(IN) - SUM(OUT) + SUM(ADJUST) + SUM(STOCKTAKE)
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS stock_movement (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
movement_date DATE NOT NULL,
|
|
warehouse_code TEXT NOT NULL REFERENCES warehouses(warehouse_code),
|
|
item_code TEXT NOT NULL CHECK (
|
|
item_code LIKE 'MT-%'
|
|
OR item_code LIKE 'MX-%'
|
|
OR item_code LIKE 'MZ-%'),
|
|
movement_type TEXT NOT NULL CHECK (movement_type IN ('IN', 'OUT', 'ADJUST', 'STOCKTAKE')),
|
|
qty INTEGER NOT NULL,
|
|
ref_type TEXT NOT NULL DEFAULT '', -- 예: shopee/lazada/stocktake/manual
|
|
ref_no TEXT NOT NULL DEFAULT '', -- 외부 주문번호/조사 id 등
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_by TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- IN/OUT 은 양수만. ADJUST/STOCKTAKE 만 음수 허용.
|
|
CHECK (
|
|
(movement_type IN ('IN', 'OUT') AND qty > 0)
|
|
OR movement_type IN ('ADJUST', 'STOCKTAKE')
|
|
)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_stock_mv_wh_item ON stock_movement (warehouse_code, item_code);
|
|
CREATE INDEX IF NOT EXISTS idx_stock_mv_date ON stock_movement (movement_date);
|
|
CREATE INDEX IF NOT EXISTS idx_stock_mv_type ON stock_movement (movement_type);
|
|
CREATE INDEX IF NOT EXISTS idx_stock_mv_ref ON stock_movement (ref_type, ref_no);
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 4-b) 콤보(세트) 출고 원장 (세트 단위)
|
|
-- set_code 는 MY- 만. 세트 출고 시 stock_movement(낱개 분해)와 함께
|
|
-- 여기에 콤보 단위 OUT 을 1행 기록한다(같은 트랜잭션).
|
|
-- 콤보 재고현황 = 최근 재고조사 콤보수량 − 재고조사일 이후 OUT 합.
|
|
-- 콤보 입고는 업무상 불가(낱개로만 입고) → OUT/ADJUST 만 허용.
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS set_movement (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
movement_date DATE NOT NULL,
|
|
warehouse_code TEXT NOT NULL REFERENCES warehouses(warehouse_code),
|
|
set_code TEXT NOT NULL CHECK (set_code LIKE 'MY-%'),
|
|
movement_type TEXT NOT NULL DEFAULT 'OUT' CHECK (movement_type IN ('OUT', 'ADJUST')),
|
|
qty INTEGER NOT NULL,
|
|
ref_type TEXT NOT NULL DEFAULT '',
|
|
ref_no TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_by TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- OUT 은 양수만. ADJUST 만 음수 허용.
|
|
CHECK ((movement_type = 'OUT' AND qty > 0) OR movement_type = 'ADJUST')
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_set_mv_wh_set ON set_movement (warehouse_code, set_code);
|
|
CREATE INDEX IF NOT EXISTS idx_set_mv_date ON set_movement (movement_date);
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 5) 일일 재고조사 (헤더)
|
|
-- 같은 날짜 + 같은 창고에 finalized 는 1건만 허용(부분 유니크 인덱스).
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS daily_stocktake (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
stocktake_date DATE NOT NULL,
|
|
warehouse_code TEXT NOT NULL REFERENCES warehouses(warehouse_code),
|
|
status TEXT NOT NULL DEFAULT 'draft'
|
|
CHECK (status IN ('draft', 'finalized', 'cancelled')),
|
|
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(),
|
|
finalized_at TIMESTAMPTZ
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_stocktake_date ON daily_stocktake (stocktake_date);
|
|
CREATE INDEX IF NOT EXISTS idx_stocktake_wh ON daily_stocktake (warehouse_code);
|
|
-- 같은 날짜+창고 finalized 중복 방지
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_stocktake_finalized
|
|
ON daily_stocktake (stocktake_date, warehouse_code)
|
|
WHERE status = 'finalized';
|
|
|
|
DROP TRIGGER IF EXISTS trg_stocktake_updated ON daily_stocktake;
|
|
CREATE TRIGGER trg_stocktake_updated
|
|
BEFORE UPDATE ON daily_stocktake
|
|
FOR EACH ROW EXECUTE FUNCTION malaysia_set_updated_at();
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 6) 일일 재고조사 라인
|
|
-- sku_code: 낱개(MT-/MX-/MZ-) 또는 세트(MY-) 허용. MD- 금지.
|
|
-- 같은 조사 안에서 같은 sku_code 중복 금지.
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS daily_stocktake_line (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
stocktake_id BIGINT NOT NULL REFERENCES daily_stocktake(id) ON DELETE CASCADE,
|
|
sku_code TEXT NOT NULL CHECK (
|
|
sku_code LIKE 'MT-%'
|
|
OR sku_code LIKE 'MX-%'
|
|
OR sku_code LIKE 'MZ-%'
|
|
OR sku_code LIKE 'MY-%'),
|
|
qty INTEGER NOT NULL CHECK (qty >= 0),
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (stocktake_id, sku_code)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_stocktake_line_st ON daily_stocktake_line (stocktake_id);
|
|
|
|
DROP TRIGGER IF EXISTS trg_stocktake_line_updated ON daily_stocktake_line;
|
|
CREATE TRIGGER trg_stocktake_line_updated
|
|
BEFORE UPDATE ON daily_stocktake_line
|
|
FOR EACH ROW EXECUTE FUNCTION malaysia_set_updated_at();
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 6.5) 일일 재고조사 랙(rack) 입력
|
|
-- 창고 랙(A/B면, 행/열/분할 칸)별로 "어떤 SKU 가 한 박스당 몇 개씩
|
|
-- 몇 박스 있는지"를 기록한다. 칸 단위 입력 → 서버가 SKU 별
|
|
-- qty = SUM(units_per_box * box_count) 로 집계해 daily_stocktake_line 을
|
|
-- 재생성한다(랙이 라인의 단일 출처). 한 칸(파렛트)에 여러 SKU 가능.
|
|
-- cell_code 예: 'A3-1-1' (면 A, 행 3, 열 1, 분할 1).
|
|
-- sku_code: 낱개(MT-/MX-/MZ-) 또는 세트(MY-) 허용. MD- 금지.
|
|
-- ════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS daily_stocktake_rack (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
stocktake_id BIGINT NOT NULL REFERENCES daily_stocktake(id) ON DELETE CASCADE,
|
|
cell_code TEXT NOT NULL, -- 랙 위치(예: A3-1-1)
|
|
line_no INTEGER NOT NULL DEFAULT 0, -- 칸 안 입력 순서
|
|
sku_code TEXT NOT NULL CHECK (
|
|
sku_code LIKE 'MT-%'
|
|
OR sku_code LIKE 'MX-%'
|
|
OR sku_code LIKE 'MZ-%'
|
|
OR sku_code LIKE 'MY-%'
|
|
OR sku_code LIKE 'MD-%'), -- 뚜껑: 위치확인용(재고 집계 제외)
|
|
units_per_box INTEGER NOT NULL CHECK (units_per_box > 0),
|
|
box_count INTEGER NOT NULL CHECK (box_count > 0),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_stocktake_rack_st ON daily_stocktake_rack (stocktake_id);
|
|
CREATE INDEX IF NOT EXISTS idx_stocktake_rack_cell ON daily_stocktake_rack (stocktake_id, cell_code);
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 7) seed — 기본 창고 + 재고관리 대상 아이템
|
|
-- ════════════════════════════════════════════════════════════
|
|
INSERT INTO warehouses (warehouse_code, warehouse_name, country) VALUES
|
|
('MY-WH-01', 'Malaysia Warehouse', 'MY')
|
|
ON CONFLICT (warehouse_code) DO NOTHING;
|
|
|
|
-- 낱개 아이템 (MT-/MX-/MZ-). MD- 뚜껑은 의도적으로 제외.
|
|
INSERT INTO malaysia_items (item_code, item_name, kind, sort_order) VALUES
|
|
('MT-0320', 'Miracle Container 320ml', 'individual', 1),
|
|
('MT-0550', 'Miracle Container 550ml', 'individual', 2),
|
|
('MT-0750', 'Miracle Container 750ml', 'individual', 3),
|
|
('MT-0950', 'Miracle Container 950ml', 'individual', 4),
|
|
('MT-1800', 'Miracle Container 1800ml', 'individual', 5),
|
|
('MT-2200', 'Miracle Container 2200ml', 'individual', 6),
|
|
('MT-3200', 'Miracle Container 3200ml', 'individual', 7),
|
|
('MT-5000', 'Miracle Container 5000ml', 'individual', 8),
|
|
('MX-0001', 'Miracle Bag 3 Types', 'individual', 9),
|
|
('MX-0003', 'Miracle Bag 5 pcs', 'individual', 10),
|
|
('MZ-0001', 'Miracle Zipperbag 2.5L', 'individual', 11),
|
|
('MZ-0002', 'Miracle Zipperbag 6.0L', 'individual', 12),
|
|
('MY-0001', 'Launching Special', 'set', 21),
|
|
('MY-0002', 'Starter Set', 'set', 22),
|
|
('MY-0003', 'Essential Set', 'set', 23),
|
|
('MY-0004', 'Premium Set', 'set', 24),
|
|
('MY-0005', 'Ultimate Set', 'set', 25),
|
|
('MY-9991', 'Combo 2.5L+6L', 'set', 26)
|
|
ON CONFLICT (item_code) DO NOTHING;
|
|
|
|
-- ════════════════════════════════════════════════════════════
|
|
-- 8) 권한 (malaysia_app: CRUD only)
|
|
-- ════════════════════════════════════════════════════════════
|
|
GRANT USAGE ON SCHEMA public TO malaysia_app;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON
|
|
warehouses, malaysia_items, set_bom, stock_movement, set_movement,
|
|
daily_stocktake, daily_stocktake_line, daily_stocktake_rack
|
|
TO malaysia_app;
|
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO malaysia_app;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO malaysia_app;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
GRANT USAGE, SELECT ON SEQUENCES TO malaysia_app;
|
|
|
|
SELECT 'malaysia_stock_db ready' AS status;
|