From 19e3cf90488198916e8b02b66028a9320e584bba Mon Sep 17 00:00:00 2001 From: chpark Date: Wed, 13 May 2026 15:48:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(statement-branch-admin):=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EB=AA=85=EC=84=B8=ED=91=9C=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4/=ED=8E=98=EC=9D=B4=EC=A7=80/API=20+=20?= =?UTF-8?q?=EC=B0=BD=EA=B3=A0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=207?= =?UTF-8?q?=EA=B0=9C=EB=A1=9C=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 마이그레이션 028: - momo_statement_branches 테이블 신설 (code PK / name / bank_account / phone / email 등) - HQ, KIMPO 기본 시드 INSERT (사용자가 관리자 페이지에서 편집 가능) - 메뉴: 시스템 그룹에 '기준 명세표 관리' (M_ASBR / menu_info 9000310) 라이브러리 (src/lib/momo-branches.ts): - 하드코딩 → DB 조회로 변경 (60초 in-memory 캐시) - getSupplierByBranch 가 async — detail/statement/approve API 도 await 추가 - 저장 시 invalidateBranchCache() 호출 페이지/API (관리자 전용): - /m/admin/statement-branches : list + 등록/수정/삭제 모달 - POST /api/m/admin/statement-branches/list - POST /api/m/admin/statement-branches/save (regist / update / delete) 사용자 수정 폼: - "기준 거래명세서" select 옵션이 하드코딩 본사/김포 → DB 의 branches list 동적 fetch 창고 관리: - WH_TYPE 카테고리 5개 → 7개 (옛 enum 도 라벨 매핑은 유지) · HQ_STOCK 본사 창고 · HQ_CHARTER 본사 용차 · HQ_MARKET 본사 시장 · KIMPO_BRANCH 김포지사 · KIMPO_STOCK 김포 창고 · KIMPO_CHARTER 김포 용차 · KIMPO_MARKET 김포 시장 - 신규 추가 시 select 는 위 7개만 노출, 기존 데이터 (STOCK 등) 는 라벨로 자연 표시 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../028_statement_branches_and_warehouses.sql | 48 +++++ .../m/admin/statement-branches/page.tsx | 203 ++++++++++++++++++ src/app/(main)/m/admin/warehouses/page.tsx | 42 +++- src/app/admin-panel/user-form/page.tsx | 10 +- .../m/admin/statement-branches/list/route.ts | 25 +++ .../m/admin/statement-branches/save/route.ts | 45 ++++ src/app/api/m/orders/approve/route.ts | 4 +- src/app/api/m/orders/detail/route.ts | 4 +- src/app/api/m/orders/statement/[id]/route.ts | 4 +- src/lib/momo-branches.ts | 63 +++++- 10 files changed, 423 insertions(+), 25 deletions(-) create mode 100644 db/migrations/028_statement_branches_and_warehouses.sql create mode 100644 src/app/(main)/m/admin/statement-branches/page.tsx create mode 100644 src/app/api/m/admin/statement-branches/list/route.ts create mode 100644 src/app/api/m/admin/statement-branches/save/route.ts diff --git a/db/migrations/028_statement_branches_and_warehouses.sql b/db/migrations/028_statement_branches_and_warehouses.sql new file mode 100644 index 0000000..18cccfb --- /dev/null +++ b/db/migrations/028_statement_branches_and_warehouses.sql @@ -0,0 +1,48 @@ +-- 1) 기준 명세표 관리 테이블 신설 (관리자가 supplier 정보 편집) +CREATE TABLE IF NOT EXISTS momo_statement_branches ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + bank_account VARCHAR(200) NOT NULL, + phone VARCHAR(40), + email VARCHAR(100), + ceo_name VARCHAR(50), + biz_no VARCHAR(40), + address VARCHAR(200), + is_default CHAR(1) DEFAULT 'N', + sort_order INTEGER DEFAULT 0, + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW() +); + +-- 기본 시드 (idempotent: code 가 PK 라 ON CONFLICT DO NOTHING 안전) +INSERT INTO momo_statement_branches (code, name, bank_account, phone, email, ceo_name, is_default, sort_order) +VALUES + ('HQ', '본사', '기업은행 434-115361-01-016 (이상용)', '010-6369-8443', 'momo8443@daum.net', '이상용', 'Y', 1), + ('KIMPO', '김포', '농협 351-1383-7634-13 (모모유통)', '010-5789-9431', 'momokimpo@nate.com', '이상용', 'N', 2) +ON CONFLICT (code) DO NOTHING; + +-- 2) 기준 명세표 관리 메뉴 (admin-panel 의 시스템 관리 또는 별도) +-- momo_menus 시스템 그룹에 추가 +INSERT INTO momo_menus (objid, menu_code, menu_name, menu_url, parent_code, sort_order, group_name, is_system, is_del, regdate) +VALUES ('M_ASBR', 'A_SBR', '기준 명세표 관리', '/m/admin/statement-branches', NULL, 92, '시스템', 'Y', 'N', NOW()) +ON CONFLICT (objid) DO UPDATE + SET menu_name = EXCLUDED.menu_name, + menu_url = EXCLUDED.menu_url, + sort_order = EXCLUDED.sort_order, + group_name = EXCLUDED.group_name, + is_del = 'N'; + +-- menu_info (FITO 사이드바 ground truth) 도 추가 — 매입/입고 부모(9000300) 와 별도, 시스템 그룹에 +-- 9000306 으로 신설, 부모 -395553955 (메뉴 root) 사용 +INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) +SELECT '9000310', '1', '-395553955', '기준 명세표 관리', 'Statement Branch', '710', '/m/admin/statement-branches', 'active', 'PMS', NOW() +WHERE NOT EXISTS (SELECT 1 FROM menu_info WHERE objid = '9000310'); + +UPDATE menu_info SET + menu_name_kor = '기준 명세표 관리', + menu_name_eng = 'Statement Branch', + parent_obj_id = '-395553955', + seq = '710', + menu_url = '/m/admin/statement-branches', + status = 'active' +WHERE objid = '9000310'; diff --git a/src/app/(main)/m/admin/statement-branches/page.tsx b/src/app/(main)/m/admin/statement-branches/page.tsx new file mode 100644 index 0000000..a236d3d --- /dev/null +++ b/src/app/(main)/m/admin/statement-branches/page.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { useEffect, useState, FormEvent } from "react"; +import { Plus, Pencil, Trash2 } from "lucide-react"; +import Swal from "sweetalert2"; + +interface Branch { + CODE: string; + NAME: string; + BANK_ACCOUNT: string; + PHONE: string | null; + EMAIL: string | null; + CEO_NAME: string | null; + BIZ_NO: string | null; + ADDRESS: string | null; + IS_DEFAULT: string; + SORT_ORDER: number; +} + +export default function StatementBranchesPage() { + const [list, setList] = useState([]); + const [editing, setEditing] = useState | null>(null); + const [isNew, setIsNew] = useState(false); + + const load = async () => { + const res = await fetch("/api/m/admin/statement-branches/list", { method: "POST" }); + setList((await res.json()).RESULTLIST ?? []); + }; + useEffect(() => { load(); }, []); + + const openNew = () => { + setIsNew(true); + setEditing({ CODE: "", NAME: "", BANK_ACCOUNT: "", IS_DEFAULT: "N", SORT_ORDER: list.length + 1 }); + }; + const openEdit = (b: Branch) => { setIsNew(false); setEditing({ ...b }); }; + + const save = async (e: FormEvent) => { + e.preventDefault(); + if (!editing) return; + if (!editing.CODE || !editing.NAME || !editing.BANK_ACCOUNT) { + Swal.fire({ icon: "warning", title: "코드/이름/계좌번호는 필수" }); + return; + } + const res = await fetch("/api/m/admin/statement-branches/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + actionType: isNew ? "regist" : "update", + code: editing.CODE, name: editing.NAME, bankAccount: editing.BANK_ACCOUNT, + phone: editing.PHONE ?? "", email: editing.EMAIL ?? "", + ceoName: editing.CEO_NAME ?? "", bizNo: editing.BIZ_NO ?? "", address: editing.ADDRESS ?? "", + isDefault: editing.IS_DEFAULT ?? "N", sortOrder: String(editing.SORT_ORDER ?? 0), + }), + }); + const j = await res.json(); + if (j.success) { + await Swal.fire({ icon: "success", title: "저장 완료", timer: 1200, showConfirmButton: false }); + setEditing(null); load(); + } else { + Swal.fire({ icon: "error", title: "저장 실패", text: j.message }); + } + }; + + const del = async (b: Branch) => { + if (b.IS_DEFAULT === "Y") { + Swal.fire({ icon: "warning", title: "기본 명세표는 삭제 불가" }); + return; + } + const ok = await Swal.fire({ icon: "question", title: `${b.NAME} (${b.CODE}) 삭제?`, showCancelButton: true }); + if (!ok.isConfirmed) return; + const res = await fetch("/api/m/admin/statement-branches/save", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ actionType: "delete", code: b.CODE, name: b.NAME, bankAccount: b.BANK_ACCOUNT }), + }); + if ((await res.json()).success) load(); + }; + + return ( +
+
+
+

기준 명세표 관리

+

거래명세표/계산서에 표시될 공급자(모모유통) 정보. 사용자별 기준 명세서 설정에서 어느 코드를 사용할지 선택.

+
+ +
+ +
+ + + + + + + + + + + + + + {list.length === 0 ? ( + + ) : list.map((b) => ( + + + + + + + + + + ))} + +
코드이름결제 계좌전화이메일기본동작
등록된 기준 명세표가 없습니다.
{b.CODE}{b.NAME}{b.BANK_ACCOUNT}{b.PHONE}{b.EMAIL} + {b.IS_DEFAULT === "Y" && 기본} + + + +
+
+ + {editing && ( +
setEditing(null)}> +
e.stopPropagation()}> +

{isNew ? "기준 명세표 등록" : `기준 명세표 수정 (${editing.CODE})`}

+
+ + setEditing({ ...editing, CODE: e.target.value.toUpperCase() })} + placeholder="HQ / KIMPO 등" className="w-full h-9 px-3 rounded border border-slate-200 text-sm font-mono disabled:bg-slate-100" /> + + + setEditing({ ...editing, NAME: e.target.value })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + + + setEditing({ ...editing, BANK_ACCOUNT: e.target.value })} + placeholder="예: 기업은행 434-115361-01-016 (이상용)" + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + +
+ + setEditing({ ...editing, PHONE: e.target.value })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + + + setEditing({ ...editing, EMAIL: e.target.value })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + +
+
+ + setEditing({ ...editing, CEO_NAME: e.target.value })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + + + setEditing({ ...editing, BIZ_NO: e.target.value })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + +
+ + setEditing({ ...editing, ADDRESS: e.target.value })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + +
+ + + setEditing({ ...editing, SORT_ORDER: Number(e.target.value) })} + className="w-full h-9 px-3 rounded border border-slate-200 text-sm" /> + +
+
+
+ + +
+
+
+ )} +
+ ); +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( + + ); +} diff --git a/src/app/(main)/m/admin/warehouses/page.tsx b/src/app/(main)/m/admin/warehouses/page.tsx index 3cee4ad..624cd26 100644 --- a/src/app/(main)/m/admin/warehouses/page.tsx +++ b/src/app/(main)/m/admin/warehouses/page.tsx @@ -7,16 +7,38 @@ import Swal from "sweetalert2"; interface Warehouse { OBJID: string; WH_CODE: string; WH_NAME: string; LOCATION: string; WH_TYPE: string; } +// 본사/김포 각각 창고/용차/시장 + 김포지사 자체 = 총 7가지 const TYPE_LABEL: Record = { - STOCK: "본사 창고", KIMPO: "김포 창고", PICKUP_TEAM: "창고픽업팀", MARKET: "시장픽업", DELIVERY: "용차배송", + HQ_STOCK: "본사 창고", + HQ_CHARTER: "본사 용차", + HQ_MARKET: "본사 시장", + KIMPO_BRANCH:"김포지사", + KIMPO_STOCK: "김포 창고", + KIMPO_CHARTER:"김포 용차", + KIMPO_MARKET:"김포 시장", + // ↓ 옛 enum (기존 데이터 표시 유지) + STOCK: "본사 창고", + KIMPO: "김포 창고", + PICKUP_TEAM: "창고픽업팀", + MARKET: "시장픽업", + DELIVERY: "용차배송", }; const TYPE_COLOR: Record = { - STOCK: "bg-emerald-100 text-emerald-700", - KIMPO: "bg-teal-100 text-teal-700", - PICKUP_TEAM: "bg-sky-100 text-sky-700", - MARKET: "bg-amber-100 text-amber-700", - DELIVERY: "bg-orange-100 text-orange-700", + HQ_STOCK: "bg-emerald-100 text-emerald-700", + HQ_CHARTER: "bg-orange-100 text-orange-700", + HQ_MARKET: "bg-amber-100 text-amber-700", + KIMPO_BRANCH: "bg-teal-100 text-teal-700", + KIMPO_STOCK: "bg-teal-100 text-teal-700", + KIMPO_CHARTER:"bg-orange-100 text-orange-700", + KIMPO_MARKET: "bg-amber-100 text-amber-700", + STOCK: "bg-emerald-100 text-emerald-700", + KIMPO: "bg-teal-100 text-teal-700", + PICKUP_TEAM: "bg-sky-100 text-sky-700", + MARKET: "bg-amber-100 text-amber-700", + DELIVERY: "bg-orange-100 text-orange-700", }; +// 신규 카테고리 (select 옵션). 옛 enum 은 데이터 표시만 유지, 신규 추가는 새 enum 만. +const NEW_TYPE_ORDER = ["HQ_STOCK", "HQ_CHARTER", "HQ_MARKET", "KIMPO_BRANCH", "KIMPO_STOCK", "KIMPO_CHARTER", "KIMPO_MARKET"]; export default function WarehousesPage() { const [list, setList] = useState([]); @@ -36,7 +58,7 @@ export default function WarehousesPage() { body: JSON.stringify({ objid: editing.OBJID, actionType: editing.OBJID ? "update" : "regist", whName: editing.WH_NAME, - location: editing.LOCATION, whType: editing.WH_TYPE || "STOCK", + location: editing.LOCATION, whType: editing.WH_TYPE || "HQ_STOCK", }), }); if ((await res.json()).success) { @@ -52,7 +74,7 @@ export default function WarehousesPage() {

창고 관리

총 {list.length}개

- @@ -129,8 +151,8 @@ export default function WarehousesPage() { /> setEditing({ ...editing, WH_NAME: e.target.value })} className="w-full h-10 px-3 rounded-lg border border-slate-200 text-sm" /> - setEditing({ ...editing, WH_TYPE: e.target.value })} className="w-full h-10 px-3 rounded-lg border border-slate-200 text-sm bg-white"> + {NEW_TYPE_ORDER.map((k) => )} setEditing({ ...editing, LOCATION: e.target.value })} className="w-full h-10 px-3 rounded-lg border border-slate-200 text-sm" /> diff --git a/src/app/admin-panel/user-form/page.tsx b/src/app/admin-panel/user-form/page.tsx index 466dd45..0530cae 100644 --- a/src/app/admin-panel/user-form/page.tsx +++ b/src/app/admin-panel/user-form/page.tsx @@ -13,6 +13,7 @@ function UserForm() { const [form, setForm] = useState>({}); const [depts, setDepts] = useState<{ DEPT_CODE: string; DEPT_NAME: string }[]>([]); const [whs, setWhs] = useState<{ OBJID: string; WH_NAME: string; WH_CODE: string }[]>([]); + const [branches, setBranches] = useState<{ CODE: string; NAME: string }[]>([]); const [loading, setLoading] = useState(false); const set = (k: string, v: string) => setForm((p) => ({ ...p, [k]: v })); @@ -21,6 +22,8 @@ function UserForm() { .then((r) => r.json()).then((d) => setDepts(d.RESULTLIST || [])).catch(() => {}); fetch("/api/m/warehouses/list", { method: "POST" }) .then((r) => r.json()).then((d) => setWhs(d.RESULTLIST || [])).catch(() => {}); + fetch("/api/m/admin/statement-branches/list", { method: "POST" }) + .then((r) => r.json()).then((d) => setBranches(d.RESULTLIST || [])).catch(() => {}); }, []); useEffect(() => { @@ -118,8 +121,11 @@ function UserForm() { onChange={(e) => set("statement_branch", e.target.value)} className="h-8 w-full rounded border border-gray-300 bg-white px-2 text-[12px]" > - - + {branches.length === 0 ? ( + + ) : branches.map((b) => ( + + ))}
diff --git a/src/app/api/m/admin/statement-branches/list/route.ts b/src/app/api/m/admin/statement-branches/list/route.ts new file mode 100644 index 0000000..b97cecf --- /dev/null +++ b/src/app/api/m/admin/statement-branches/list/route.ts @@ -0,0 +1,25 @@ +// 기준 명세표 (공급자 정보) 목록 — admin +import { NextResponse } from "next/server"; +import { queryRows } from "@/lib/db"; +import { requireMomoAdmin } from "@/lib/momo-guard"; + +export async function POST() { + const g = await requireMomoAdmin(); + if (g instanceof NextResponse) return g; + const rows = await queryRows( + `SELECT code AS "CODE", + name AS "NAME", + bank_account AS "BANK_ACCOUNT", + phone AS "PHONE", + email AS "EMAIL", + ceo_name AS "CEO_NAME", + biz_no AS "BIZ_NO", + address AS "ADDRESS", + COALESCE(is_default, 'N') AS "IS_DEFAULT", + COALESCE(sort_order, 0) AS "SORT_ORDER" + FROM momo_statement_branches + WHERE COALESCE(is_del,'N') != 'Y' + ORDER BY sort_order ASC, code ASC` + ); + return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length }); +} diff --git a/src/app/api/m/admin/statement-branches/save/route.ts b/src/app/api/m/admin/statement-branches/save/route.ts new file mode 100644 index 0000000..18779e3 --- /dev/null +++ b/src/app/api/m/admin/statement-branches/save/route.ts @@ -0,0 +1,45 @@ +// 기준 명세표 (공급자 정보) 저장 — admin +import { NextRequest, NextResponse } from "next/server"; +import { execute } from "@/lib/db"; +import { requireMomoAdmin } from "@/lib/momo-guard"; +import { invalidateBranchCache } from "@/lib/momo-branches"; + +export async function POST(req: NextRequest) { + const g = await requireMomoAdmin(); + if (g instanceof NextResponse) return g; + const body = await req.json().catch(() => ({})); + const { + code, name, bankAccount, phone, email, ceoName, bizNo, address, + isDefault, sortOrder, actionType, + } = body as Record; + + if (!code) return NextResponse.json({ success: false, message: "code 필수" }, { status: 400 }); + if (!name) return NextResponse.json({ success: false, message: "name 필수" }, { status: 400 }); + if (!bankAccount) return NextResponse.json({ success: false, message: "bankAccount 필수" }, { status: 400 }); + + if (actionType === "delete") { + await execute(`UPDATE momo_statement_branches SET is_del='Y' WHERE code=$1`, [code]); + } else if (actionType === "regist") { + await execute( + `INSERT INTO momo_statement_branches (code, name, bank_account, phone, email, ceo_name, biz_no, address, is_default, sort_order, is_del, regdate) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'N',NOW()) + ON CONFLICT (code) DO UPDATE SET + name=EXCLUDED.name, bank_account=EXCLUDED.bank_account, + phone=EXCLUDED.phone, email=EXCLUDED.email, + ceo_name=EXCLUDED.ceo_name, biz_no=EXCLUDED.biz_no, + address=EXCLUDED.address, is_default=EXCLUDED.is_default, + sort_order=EXCLUDED.sort_order, is_del='N'`, + [code, name, bankAccount, phone ?? null, email ?? null, ceoName ?? null, bizNo ?? null, address ?? null, isDefault === "Y" ? "Y" : "N", Number(sortOrder) || 0] + ); + } else { + await execute( + `UPDATE momo_statement_branches SET + name=$2, bank_account=$3, phone=$4, email=$5, ceo_name=$6, biz_no=$7, address=$8, + is_default=$9, sort_order=$10 + WHERE code=$1`, + [code, name, bankAccount, phone ?? null, email ?? null, ceoName ?? null, bizNo ?? null, address ?? null, isDefault === "Y" ? "Y" : "N", Number(sortOrder) || 0] + ); + } + invalidateBranchCache(); + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/m/orders/approve/route.ts b/src/app/api/m/orders/approve/route.ts index cbe9285..9298e1f 100644 --- a/src/app/api/m/orders/approve/route.ts +++ b/src/app/api/m/orders/approve/route.ts @@ -139,8 +139,8 @@ export async function POST(req: NextRequest) { bizNo: order.biz_no as string | undefined, phone: order.phone as string | undefined, }, - supplier: (() => { - const b = getSupplierByBranch(order.statement_branch as string | undefined); + supplier: await (async () => { + const b = await getSupplierByBranch(order.statement_branch as string | undefined); return { companyName: b.NAME, bankAccount: b.BANK_ACCOUNT, phone: b.PHONE, email: b.EMAIL }; })(), items: items.map((it) => ({ diff --git a/src/app/api/m/orders/detail/route.ts b/src/app/api/m/orders/detail/route.ts index 4c98d3e..817502f 100644 --- a/src/app/api/m/orders/detail/route.ts +++ b/src/app/api/m/orders/detail/route.ts @@ -77,9 +77,9 @@ export async function POST(req: NextRequest) { [objid] ); - // 공급자(모모유통) 정보 — 거래처(사용자)의 statement_branch (HQ / KIMPO) 에 따라 분기 + // 공급자(모모유통) 정보 — 거래처(사용자)의 statement_branch 에 따라 DB 분기 const branch = (order as { STATEMENT_BRANCH?: string }).STATEMENT_BRANCH ?? "HQ"; - const supplier = getSupplierByBranch(branch); + const supplier = await getSupplierByBranch(branch); return NextResponse.json({ success: true, order, items, supplier }); } diff --git a/src/app/api/m/orders/statement/[id]/route.ts b/src/app/api/m/orders/statement/[id]/route.ts index 438ce04..2ce23f8 100644 --- a/src/app/api/m/orders/statement/[id]/route.ts +++ b/src/app/api/m/orders/statement/[id]/route.ts @@ -57,8 +57,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string bizNo: order.biz_no as string | undefined, phone: order.phone as string | undefined, }, - supplier: (() => { - const b = getSupplierByBranch(order.statement_branch as string | undefined); + supplier: await (async () => { + const b = await getSupplierByBranch(order.statement_branch as string | undefined); return { companyName: b.NAME, bankAccount: b.BANK_ACCOUNT, phone: b.PHONE, email: b.EMAIL }; })(), items: items.map((it, idx) => { diff --git a/src/lib/momo-branches.ts b/src/lib/momo-branches.ts index 583f6d4..b7294f9 100644 --- a/src/lib/momo-branches.ts +++ b/src/lib/momo-branches.ts @@ -1,8 +1,13 @@ -// 모모유통 공급자(supplier) 정보 — 거래명세표/계산서 발행 시 사용자별로 분기 -// 사용자(user_info.statement_branch) 가 'HQ' / 'KIMPO' 중 어느 값이냐에 따라 적용 -export type StatementBranch = "HQ" | "KIMPO"; +// 모모유통 공급자(supplier) 정보 — DB(momo_statement_branches) 기반 +// 거래명세표/계산서 발행 시 사용자(user_info.statement_branch) 별로 분기 +// +// 관리자가 /m/admin/statement-branches 페이지에서 정보 편집 가능. +// API 요청 시마다 DB 조회하면 부담이라 60초 in-memory 캐시. + +import { queryRows } from "@/lib/db"; export interface BranchSupplier { + CODE: string; NAME: string; CEO: string; BANK_ACCOUNT: string; @@ -12,8 +17,10 @@ export interface BranchSupplier { ADDRESS: string; } -export const SUPPLIER_BRANCHES: Record = { +// fallback (DB 가 비어있거나 마이그레이션 028 적용 전 환경) +const FALLBACK: Record = { HQ: { + CODE: "HQ", NAME: "모모유통", CEO: "이상용", BANK_ACCOUNT: "기업은행 434-115361-01-016 (이상용)", @@ -23,6 +30,7 @@ export const SUPPLIER_BRANCHES: Record = { ADDRESS: "", }, KIMPO: { + CODE: "KIMPO", NAME: "모모유통", CEO: "이상용", BANK_ACCOUNT: "농협 351-1383-7634-13 (모모유통)", @@ -33,7 +41,48 @@ export const SUPPLIER_BRANCHES: Record = { }, }; -export function getSupplierByBranch(branch?: string | null): BranchSupplier { - const key = (branch === "KIMPO" ? "KIMPO" : "HQ") as StatementBranch; - return SUPPLIER_BRANCHES[key]; +let cache: { at: number; map: Record } | null = null; +const CACHE_TTL_MS = 60_000; + +async function loadBranches(): Promise> { + if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache.map; + try { + const rows = await queryRows<{ + CODE: string; NAME: string; CEO: string | null; BANK_ACCOUNT: string; + PHONE: string | null; EMAIL: string | null; BIZ_NO: string | null; ADDRESS: string | null; + }>( + `SELECT code AS "CODE", name AS "NAME", ceo_name AS "CEO", + bank_account AS "BANK_ACCOUNT", phone AS "PHONE", + email AS "EMAIL", biz_no AS "BIZ_NO", address AS "ADDRESS" + FROM momo_statement_branches + WHERE COALESCE(is_del,'N') != 'Y'` + ); + const map: Record = {}; + for (const r of rows) { + map[r.CODE] = { + CODE: r.CODE, + NAME: r.NAME, + CEO: r.CEO ?? "", + BANK_ACCOUNT: r.BANK_ACCOUNT, + PHONE: r.PHONE ?? "", + EMAIL: r.EMAIL ?? "", + BIZ_NO: r.BIZ_NO ?? "", + ADDRESS: r.ADDRESS ?? "", + }; + } + cache = { at: Date.now(), map: { ...FALLBACK, ...map } }; + return cache.map; + } catch { + return FALLBACK; + } +} + +export async function getSupplierByBranch(branch?: string | null): Promise { + const code = branch || "HQ"; + const map = await loadBranches(); + return map[code] ?? map.HQ ?? FALLBACK.HQ; +} + +export function invalidateBranchCache() { + cache = null; }