Connect 판매출고 거래처 selector to customer_mng (COMPANY_7/8/9/30)

영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블을 사용하도록
SalesOutbound 의 두 거래처 입력 경로를 연결.

- fetchAllCustomers: 빈 배열 → POST /table-management/tables/customer_mng/data
  (page 1, size 500, autoFilter, sort customer_code desc)
- <SupplierModal />: default supplier_mng 사용 중지, source prop 으로
  customer_mng 매핑 전달 (code/name/business_number/contact_phone/address)
- 4사 동일 패치, 패치 후 md5 일치 확인
- COMPANY_7 풀검증 + COMPANY_8 spot 검증 (인라인+모달 customer_mng 200 OK,
  supplier_mng 호출 0건, 콘솔 에러 0건)
- POP.md 작업 로그 갱신

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kmh
2026-04-29 18:53:40 +09:00
parent 2cd32e312e
commit 5a2fd87b3d
5 changed files with 251 additions and 12 deletions

View File

@@ -3,11 +3,24 @@
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { useRouter } from "next/navigation";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
import { BarcodeScanModal } from "../common/BarcodeScanModal";
import type { CartItemWithId } from "../common/useCartSync";
import { COLOR_MAP } from "../common/theme";
import { apiClient } from "@/lib/api/client";
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
const CUSTOMER_SOURCE: PartnerSourceConfig = {
tableName: "customer_mng",
fields: {
code: "customer_code",
name: "customer_name",
businessNumber: "business_number",
phone: "contact_phone",
address: "address",
},
};
/* ------------------------------------------------------------------ */
/* Types */
@@ -83,10 +96,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
const customerDropdownRef = useRef<HTMLDivElement>(null);
/* Fetch all customers for inline search
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
*/
const fetchAllCustomers = useCallback(async () => {
setAllCustomers([]);
try {
const res = await apiClient.post(
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
{
page: 1,
size: 500,
autoFilter: true,
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
},
);
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
(r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
}),
);
setAllCustomers(list);
} catch {
setAllCustomers([]);
}
}, []);
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
@@ -543,6 +580,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
open={customerModalOpen}
onClose={() => setCustomerModalOpen(false)}
onSelect={(s) => selectCustomer(s)}
source={CUSTOMER_SOURCE}
/>
<SimpleKeypadModal

View File

@@ -186,6 +186,93 @@ frontend/app/(main)/COMPANY_8/pop/ <- 업체별 커스터마이징 자유
## 작업 로그
### 2026-04-29
- **판매출고 거래처 API 연결 (COMPANY_7/8/9/30 4사 모두 적용 완료)**
- [수정] `frontend/app/(main)/COMPANY_{7,8,9,30}/pop/_components/outbound/SalesOutbound.tsx` (4파일 동일 패치, 패치 후 md5 모두 동일)
- 인라인 검색창 자동완성 (`fetchAllCustomers`): 빈 배열 → `POST /table-management/tables/customer_mng/data` (page 1, size 500, autoFilter, sort customer_code desc) 연동
- "거래처 선택" 모달 (`<SupplierModal />`): default `supplier_mng` 사용 → `source={CUSTOMER_SOURCE}` prop 전달하여 `customer_mng` 사용. 컬럼 매핑은 영업관리 > 거래처관리 화면(`sales/customer/page.tsx`)과 동일 (code/name/business_number/contact_phone/address)
- SupplierModal 컴포넌트 자체 미수정 — 호출처에서 source prop만 전달
- 검증: `npx tsc --noEmit` SalesOutbound 관련 0건. COMPANY_7 풀검증(거래처 모달 8건 표시: 거래처명(수주처)/(주)노루페인트/서울경기영업소/(주)세이프코리아/오정정밀/지원(천안)/(주)탑씰/test) + COMPANY_8 spot 검증(인라인 + 모달 호출 모두 customer_mng 200 OK). 4파일 모두 supplier_mng 호출 0건 / 콘솔 에러 0건
- **fetchOrders는 별도 TODO** — 이번 지시 범위 아님, 빈 배열 유지
- **재고조정 화면 — UI 골격 (COMPANY_7, API 미연결)**
- [신규] [inventory/adjust/page.tsx](inventory/adjust/page.tsx): 헤더(뒤로가기 + 타이틀 + 우측 "이력조회" 버튼) + 좌측(창고 칩 4개 / 키워드 검색 / 카드 그리드 2열) + 우측("처리 결과" 패널 + 초기화/임시저장/일괄 확정 버튼)
- [수정] [inventory/page.tsx](inventory/page.tsx): `handleManageClick``adjust` 분기 추가 (move 분기는 미연결 유지)
- **모든 데이터는 정적 mock**: 창고 칩(전체/맹동창고/외주창고/테스트), 카드 9건, 결과 1건. apiClient/훅/저장 로직 일절 없음. 모든 클릭 핸들러는 `() => { /* TODO: API 연결 */ }` 빈 함수
- PopShell 은 layout 에서 이미 적용됨 (POP.md 0-5 룰 준수, layout 미수정). 페이지 내부에 헤더 행 직접 렌더 (inout-manage 패턴)
- **백엔드 미작업**: 기존 `popInventoryController``getStockDetail`/`adjustBatch`/`getAdjustHistory` 3개 함수는 호출처 검증 결과 모두 deprecated `components/pop/hardcoded/` 에서만 사용 — SeongHyun Kim (퇴사) 단독 작성, 신 POP 재고조정용 백엔드 신규/재정렬 여부는 사용자 결정 대기
- 검증: 신규 파일 타입체크 에러 0건 / 브라우저 검증 대기
- **공정 접수 모달 max 0 EA 버그 — available-qty 와 accept-process 정책 일치**
- 증상: 비필수 공정을 건너뛰고 다음 필수 공정 접수 시 모달에 "최대 0 EA" 표시 + "접수가능량(0)을 초과합니다" 차단
- 원인: [popProductionController.ts](../../../../backend-node/src/controllers/popProductionController.ts) `getAvailableQty` (모달 max 조회) 가 `getPrevProcessGoodQty` 를 호출 → **단순 직전 seq 의 양품 SUM 만 봄** (비필수 wop_result 0건 자동 skip 정책 미반영). 반면 `acceptProcess` 본 처리는 `evaluatePrevProcesses` 호출하여 비필수 0건 자동 skip 후 그 이전 필수 공정 양품 사용. 두 함수 정책 불일치로 모달 단계에서 잘못된 0 노출
- 수정: `getAvailableQty``evaluatePrevProcesses` 사용으로 통일. 4줄 변경 ([popProductionController.ts:2017-2022](../../../../backend-node/src/controllers/popProductionController.ts#L2017-L2022))
- 영향: backend-node 한 파일, 함수 시그니처 변경 없음. `getPrevProcessGoodQty` 함수 선언은 잔존 (호출처 없음 — 추후 정리 가능)
- 검증: backend `npm run build` 성공 / 사용자 브라우저 재현 검증 대기
- **입출고 상세 모달 — 거래처 코드 제거 + 작업자 이름 표시 (COMPANY_7 우선 검증)**
- **백엔드**: [popInOutDetailController.ts](../../../../backend-node/src/controllers/popInOutDetailController.ts) inbound/outbound 쿼리에 `LEFT JOIN user_info u ON u.user_id = im.writer AND u.company_code = im.company_code` 추가, `u.user_name AS writer_name` 셀렉트. history 는 기존 `manager_name` 유지 (변경 없음). 7개 회사 공통 사용 API
- **프론트 API 타입**: [lib/api/popInOutDetail.ts](../../../../frontend/lib/api/popInOutDetail.ts) `InboundHeader` / `OutboundRow``writer_name: string | null` 추가
- **모달**: [COMPANY_7/_components/inventory/InOutDetailModal.tsx](_components/inventory/InOutDetailModal.tsx)
- 거래처 행: `joinPartner(code, name)``name` 만 표시 (입고/출고 양쪽). 코드 우측 `(SUP-0007)` 제거
- 작업자 행: `h.writer``h.writer_name || h.writer` (입고), `r.writer``r.writer_name || r.writer` (출고). user_info 매칭 실패 시 ID fallback
- history 분기는 미변경 (이미 `manager_name || writer` 사용)
- `joinPartner` 함수는 unused 가 되었지만 유지 (다른 회사 모달 일괄 적용 시까지 보존)
- **다른 6개 회사 (COMPANY_8/9/10/16/29/30) 일괄 확대 적용** (COMPANY_7 사용자 검증 통과 후 동일 패치)
- 각 회사 `_components/inventory/InOutDetailModal.tsx` 의 inbound/outbound 거래처 행 (`joinPartner(code, name)``name`) 및 작업자 행 (`writer``writer_name || writer`) 동일 변경
- history 분기는 기존 `manager_name || writer` 유지
- `joinPartner` 함수 선언부만 unused 상태로 7개 회사 모두 잔존 (다른 호출처 없음 — 추후 일괄 정리 가능)
- 검증: `tsc --noEmit` 신규 에러 0 / `backend npm run build` 성공
### 2026-04-28
- **입출고관리 화면 신규 (UI 껍데기만)**
- `inventory/inout-manage/page.tsx` 신규: 헤더(뒤로가기 + 박스 아이콘 + "입출고관리") + 필터 카드(입고일/유형/거래처/검색 + 검색 버튼) + KPI 4개(입고/출고/이동/전체) + 카드 그리드(direction별 색상 — 입고 green / 출고 blue / 이동 amber)
- mock 카드 12건 + KPI 숫자 mock(12/8/3/23). POP.md 0-2 룰 준수: `apiClient`/훅/모달/`useCartSync` 없음, 핸들러는 빈 함수 + `// TODO: API 연결` 주석만
- 헤더는 page 내부 직접 렌더 (POP.md 0-5)
- `inventory/page.tsx` 메뉴 라우팅 연결: `handleManageClick``id === "inout-manage"` 분기 추가 → `companyPath("/pop/inventory/inout-manage")` push. `adjust`/`move` 는 빈 분기로 유지 (`// TODO`)
- 검증: `tsc --noEmit` 신규 에러 0 (baseline 유지)
- **입출고관리 화면 — 옵션 D (3-way UNION) API 연결 + UI 다듬기 (이어서)**
- **백엔드 신규**: [popInOutHistoryController.ts](../../../../backend-node/src/controllers/popInOutHistoryController.ts) 3-way UNION CTE — (1) inbound_mng+inbound_detail 평탄화 / (2) outbound_mng / (3) inventory_history 중 `transaction_type IN ('조정','조정확인','이동','공정입고')`만 (입고/출고는 (1)(2)와 중복이라 제외)
- 라우트 1줄 추가: [popInventoryRoutes.ts](../../../../backend-node/src/routes/popInventoryRoutes.ts) `GET /pop/inventory/inout-history`
- 응답: `rows + kpi(inbound/outbound/transfer/total) + totalPages + currentPage`. 파라미터: `date_from`/`date_to`/`keyword`/`direction`/`warehouse_code`/`page`/`page_size`
- 멀티테넌시: `$1::text = '*' OR company_code = $1` (슈퍼관리자 처리). 모든 파라미터 바인딩 $1..$N
- **KPI는 direction 필터 미적용** (전체 카운트 유지) / 데이터+페이지수만 direction 적용 → "전체" KPI가 클릭마다 변하는 버그 수정
- keyword 검색은 `item_name + item_code` 만 (doc_number 제외 — 사용자 옵션 A 결정. 카드에 없는 doc_number 매칭으로 인한 혼란 제거)
- SQL 컬럼: `id.id AS detail_id` 추가 (UNION 컬럼 정렬 위해 outbound/history는 `NULL::text`). React key `${source}-${doc_id}-${detail_id ?? "0"}` 로 detail 다중 row 시 충돌 해결. KPI 카운트는 detail 평탄화 기준(B 옵션) 유지
- **프론트 API client 신규**: [lib/api/popInOutHistory.ts](../../../../frontend/lib/api/popInOutHistory.ts) — `InOutRow`/`InOutKpi`/`InOutHistoryData`/`InOutHistoryParams` 타입 + `getInOutHistory` 함수
- **page 본체 변경**:
- mock 제거 → `useState/useCallback/useEffect` 기반 API. `dateInput/warehouseInput/keywordInput` (입력값) vs `appliedFilter` (적용값) 분리, `handleSearch` 시 갱신 + `page=1` 리셋
- 카테고리 라벨 매핑 `useCategoryLabelMap([{inbound_mng/inbound_type},{outbound_mng/outbound_type}])` → RecordCard `typeLabel` 영문 코드(`CAT_MLYTB8ON_A3AU`) → 한글("구매입고") 변환
- 창고 select (DB 동적): `getReceivingWarehouses()` 1회 fetch + `warehouse_code` 필터
- KPI 4개 카드 button 화 (`active`/`onClick` prop): 클릭 시 `handleDirectionFilter(direction)` 즉시 적용 + `page=1`, active = `border-blue-500 ring-2 ring-blue-200`
- 새로고침 버튼: `fetchData()` + `disabled={loading}` + SVG `animate-spin` + "동기화중..."/"새로고침"
- 페이지네이션 1 2 3 ··· N »` 패턴 — `getPaginationItems(current,total)` helper (총 7개 이하면 전부 / `current ≤ 4` / `≥ total-3` / 중간 분기). 위치: "입출고 내역" 헤더 우측 (`justify-between`)
- 카드 그리드 `grid-cols-3` 고정 (3x3) + touch swipe (`SWIPE_THRESHOLD=50`) + 양옆 보조 화살표
- **카드 디자인**: 위 줄 = 색상박스 + 종류 + 창고(작은 회색) + 수량(우측, `text-lg font-bold`) / 아래 줄 = `품목코드 · 품목명` (`text-black truncate`)
- **색상박스**: KPI 카드와 동일 스타일 — `bg-{color}-50 text-{color}-600` (그라디언트→연한 배경+진한 아이콘)
- **KPI 카드**: 본문 `h-12` (48px) 고정, 숫자 `text-[35px] leading-none`, 좌우 균등 분배 `justify-evenly`. 순서: 전체 → 입고 → 출고 → 이동
- **필터 변경**: 입고유형/거래처 select 제거 → 창고명 드롭다운 추가 (총 3컬럼: 입고일/창고명/검색). 입고일 기본값 = `new Date().toISOString().slice(0,10)` (오늘)
- **알려진 한계 / 미구현**:
- 카드 클릭 시 모달 디테일 fetch — 미구현 (`// TODO: 모달 단계`)
- native `<select>` 옵션 영역 사이즈 키우기 — OS native 컨트롤이라 CSS 제어 불가 (사용자 결정으로 보류)
- 입고일 필터는 단일 일자 (date_from = date_to). 날짜 범위 미지원
- 검증: backend/frontend `tsc --noEmit` 신규 에러 0 (양쪽 baseline 유지)
- **다른 6개 회사 일괄 적용 (COMPANY_8/9/10/16/29/30)**
- 각 회사에 `inventory/page.tsx` + `inventory/inout-manage/page.tsx` 신규 복사 (COMPANY_7 원본 그대로 — `usePopCompanyPath` 훅이 회사 코드 동적 처리하므로 파일 내용 동일)
- 각 회사 `main/page.tsx``inventory` 메뉴 `href: "#"``"/pop/inventory"` (perl multi-line `[\s\S]*?` 패턴, `strokeWidth={1.5}` JSX 표현식의 `}` 회피)
- layout.tsx / 다른 화면 (inbound/outbound/production) 미수정. POP.md 다른 회사용 별도 생성 안 함 (룰 공유)
- 백엔드 변경 0 (멀티테넌시 `company_code` 처리로 모든 회사 동일 API 사용)
- 검증: `tsc --noEmit` 신규 에러 0 (logistics baseline 에러는 무관)
### 2026-04-27
- **재고 화면 신규 (UI 껍데기만)**
- `main/page.tsx` L88: `inventory` 메뉴 `href: "#"``"/pop/inventory"` 1줄 변경 (메인 메뉴 → 재고 화면 라우팅 연결)
- `inventory/page.tsx` 신규: 헤더(뒤로가기 + 재고/입출고 현황 및 재고 관리) + Summary 카드(금일 입고/금일 출고/전체, 모두 0 고정) + 재고 관리 섹션(입출고관리/재고조정/재고이동, 빈 핸들러 `() => {}`). "최근 입출고" 섹션은 사용자 지시에 따라 미포함
- 데이터/로직 일절 미이식. POP.md 0-2 룰 준수: `apiClient`/훅/모달 없음, 핸들러는 `// TODO: API 연결` 주석만
- 헤더는 page 내부 직접 렌더 (POP.md 0-5)
### 2026-04-25
- **WorkOrderList + ProcessWork 리팩토링 4건 (Fix #4, #5, #10, #11)**
- Fix #4: `CompressedProcessSteps` completed 분기 — batchSplits status 기반 → 각 마스터 seq에 confirmed virtual split 1건 이상 기준으로 재작성

View File

@@ -3,11 +3,24 @@
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { useRouter } from "next/navigation";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
import { BarcodeScanModal } from "../common/BarcodeScanModal";
import type { CartItemWithId } from "../common/useCartSync";
import { COLOR_MAP } from "../common/theme";
import { apiClient } from "@/lib/api/client";
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
const CUSTOMER_SOURCE: PartnerSourceConfig = {
tableName: "customer_mng",
fields: {
code: "customer_code",
name: "customer_name",
businessNumber: "business_number",
phone: "contact_phone",
address: "address",
},
};
/* ------------------------------------------------------------------ */
/* Types */
@@ -83,10 +96,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
const customerDropdownRef = useRef<HTMLDivElement>(null);
/* Fetch all customers for inline search
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
*/
const fetchAllCustomers = useCallback(async () => {
setAllCustomers([]);
try {
const res = await apiClient.post(
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
{
page: 1,
size: 500,
autoFilter: true,
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
},
);
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
(r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
}),
);
setAllCustomers(list);
} catch {
setAllCustomers([]);
}
}, []);
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
@@ -543,6 +580,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
open={customerModalOpen}
onClose={() => setCustomerModalOpen(false)}
onSelect={(s) => selectCustomer(s)}
source={CUSTOMER_SOURCE}
/>
<SimpleKeypadModal

View File

@@ -3,11 +3,24 @@
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { useRouter } from "next/navigation";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
import { BarcodeScanModal } from "../common/BarcodeScanModal";
import type { CartItemWithId } from "../common/useCartSync";
import { COLOR_MAP } from "../common/theme";
import { apiClient } from "@/lib/api/client";
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
const CUSTOMER_SOURCE: PartnerSourceConfig = {
tableName: "customer_mng",
fields: {
code: "customer_code",
name: "customer_name",
businessNumber: "business_number",
phone: "contact_phone",
address: "address",
},
};
/* ------------------------------------------------------------------ */
/* Types */
@@ -83,10 +96,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
const customerDropdownRef = useRef<HTMLDivElement>(null);
/* Fetch all customers for inline search
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
*/
const fetchAllCustomers = useCallback(async () => {
setAllCustomers([]);
try {
const res = await apiClient.post(
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
{
page: 1,
size: 500,
autoFilter: true,
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
},
);
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
(r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
}),
);
setAllCustomers(list);
} catch {
setAllCustomers([]);
}
}, []);
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
@@ -543,6 +580,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
open={customerModalOpen}
onClose={() => setCustomerModalOpen(false)}
onSelect={(s) => selectCustomer(s)}
source={CUSTOMER_SOURCE}
/>
<SimpleKeypadModal

View File

@@ -3,11 +3,24 @@
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { useRouter } from "next/navigation";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
import { BarcodeScanModal } from "../common/BarcodeScanModal";
import type { CartItemWithId } from "../common/useCartSync";
import { COLOR_MAP } from "../common/theme";
import { apiClient } from "@/lib/api/client";
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
const CUSTOMER_SOURCE: PartnerSourceConfig = {
tableName: "customer_mng",
fields: {
code: "customer_code",
name: "customer_name",
businessNumber: "business_number",
phone: "contact_phone",
address: "address",
},
};
/* ------------------------------------------------------------------ */
/* Types */
@@ -83,10 +96,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
const customerDropdownRef = useRef<HTMLDivElement>(null);
/* Fetch all customers for inline search
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
*/
const fetchAllCustomers = useCallback(async () => {
setAllCustomers([]);
try {
const res = await apiClient.post(
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
{
page: 1,
size: 500,
autoFilter: true,
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
},
);
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
(r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
}),
);
setAllCustomers(list);
} catch {
setAllCustomers([]);
}
}, []);
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
@@ -543,6 +580,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
open={customerModalOpen}
onClose={() => setCustomerModalOpen(false)}
onSelect={(s) => selectCustomer(s)}
source={CUSTOMER_SOURCE}
/>
<SimpleKeypadModal