feat: 가맹점 관리의 업종을 공통코드 FRCS_TPBIZ_CD의 이름으로 보인다
업종 값은 BizPlay FRCS_TPBIZ_CD(CHAR라 뒤 공백 제거)이고 그동안 구분코드(FRCS_DV_CD)를 읽어 하드코딩 목록과 맞추고 있었다. 공통코드 FRCS_TPBIZ_CD를 받아 목록 열·상세 모달 선택지·엑셀이 같은 표로 이름을 찾는다(사용자 확정). Co-Authored-By: Claude Opus 5
@b4b2212add25982b9d18920150cf0d229b695c11
--- app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
... | ... | @@ -17,8 +17,8 @@ |
| 17 | 17 |
import { FoxSelect } from '@fox/core/components/fox-select';
|
| 18 | 18 |
import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
| 19 | 19 |
import { useFeedback } from '@/app/_hooks/use-feedback';
|
| 20 |
+import type { CommonCode } from '@/lib/domain/common-code';
|
|
| 20 | 21 |
import {
|
| 21 |
- MERCHANT_CATEGORY_OPTIONS, |
|
| 22 | 22 |
formatOptionalValue, |
| 23 | 23 |
type Merchant, |
| 24 | 24 |
} from '@/lib/domain/merchant'; |
... | ... | @@ -43,10 +43,12 @@ |
| 43 | 43 |
|
| 44 | 44 |
interface MerchantFormModalProps {
|
| 45 | 45 |
merchant?: Merchant; |
| 46 |
+ /** 업종 코드표 — 선택지이자 조회 전용일 때의 이름 출처. */ |
|
| 47 |
+ businessCodes: CommonCode[]; |
|
| 46 | 48 |
onClose: () => void; |
| 47 | 49 |
} |
| 48 | 50 |
|
| 49 |
-export function MerchantFormModal({ merchant, onClose }: MerchantFormModalProps) {
|
|
| 51 |
+export function MerchantFormModal({ merchant, businessCodes, onClose }: MerchantFormModalProps) {
|
|
| 50 | 52 |
const isEdit = Boolean(merchant); |
| 51 | 53 |
const isReadOnly = isEdit && !IS_EDIT_ENABLED; |
| 52 | 54 |
// 조회 전용일 때 입력칸마다 얹는다. `state="view"`는 시안의 조회 상태로, 잠긴 것(disabled)과 |
... | ... | @@ -201,10 +203,7 @@ |
| 201 | 203 |
size="md" |
| 202 | 204 |
label="업종" |
| 203 | 205 |
requirement="required" |
| 204 |
- options={MERCHANT_CATEGORY_OPTIONS.map((option) => ({
|
|
| 205 |
- value: option.value, |
|
| 206 |
- label: option.label, |
|
| 207 |
- }))} |
|
| 206 |
+ options={businessCodes.map((code) => ({ value: code.code, label: code.label }))}
|
|
| 208 | 207 |
value={categoryCode}
|
| 209 | 208 |
onValueChange={setCategoryCode}
|
| 210 | 209 |
placeholder="선택" |
--- app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
... | ... | @@ -9,6 +9,7 @@ |
| 9 | 9 |
import { FoxSelect } from '@fox/core/components/fox-select';
|
| 10 | 10 |
import { FoxSelectText } from '@fox/core/components/fox-select-text';
|
| 11 | 11 |
import { FoxStatusIndicator } from '@fox/core/components/fox-status-indicator';
|
| 12 |
+import type { CommonCode } from '@/lib/domain/common-code';
|
|
| 12 | 13 |
import {
|
| 13 | 14 |
formatAddressSummary, |
| 14 | 15 |
formatCategoryLabel, |
... | ... | @@ -32,6 +33,8 @@ |
| 32 | 33 |
|
| 33 | 34 |
interface MerchantListProps {
|
| 34 | 35 |
items: Merchant[]; |
| 36 |
+ /** 업종 코드표(`FRCS_TPBIZ_CD`) — 업종 열과 상세 모달이 이름을 찾는다. */ |
|
| 37 |
+ businessCodes: CommonCode[]; |
|
| 35 | 38 |
query: MerchantQuery; |
| 36 | 39 |
currentPage: number; |
| 37 | 40 |
totalPages: number; |
... | ... | @@ -43,6 +46,7 @@ |
| 43 | 46 |
|
| 44 | 47 |
export function MerchantList({
|
| 45 | 48 |
items, |
| 49 |
+ businessCodes, |
|
| 46 | 50 |
query, |
| 47 | 51 |
currentPage, |
| 48 | 52 |
totalPages, |
... | ... | @@ -73,7 +77,7 @@ |
| 73 | 77 |
key: 'categoryCode', |
| 74 | 78 |
header: '업종', |
| 75 | 79 |
width: 100, |
| 76 |
- render: (row) => formatCategoryLabel(row.categoryCode), |
|
| 80 |
+ render: (row) => formatCategoryLabel(businessCodes, row.categoryCode), |
|
| 77 | 81 |
}, |
| 78 | 82 |
{
|
| 79 | 83 |
key: 'phoneNumber', |
... | ... | @@ -111,7 +115,7 @@ |
| 111 | 115 |
key: 'actions', |
| 112 | 116 |
header: '관리', |
| 113 | 117 |
width: 120, |
| 114 |
- render: (row) => <MerchantRowActions merchant={row} />,
|
|
| 118 |
+ render: (row) => <MerchantRowActions merchant={row} businessCodes={businessCodes} />,
|
|
| 115 | 119 |
}, |
| 116 | 120 |
]; |
| 117 | 121 |
|
--- app/(protected)/(basic)/system/merchants/_components/merchant-row-actions.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-row-actions.tsx
... | ... | @@ -4,11 +4,13 @@ |
| 4 | 4 |
import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
|
| 5 | 5 |
import { FoxIconButton } from '@fox/core/components/fox-icon-button';
|
| 6 | 6 |
import { useFeedback } from '@/app/_hooks/use-feedback';
|
| 7 |
+import type { CommonCode } from '@/lib/domain/common-code';
|
|
| 7 | 8 |
import type { Merchant } from '@/lib/domain/merchant';
|
| 8 | 9 |
import { MerchantFormModal } from './merchant-form-modal';
|
| 9 | 10 |
|
| 10 | 11 |
interface MerchantRowActionsProps {
|
| 11 | 12 |
merchant: Merchant; |
| 13 |
+ businessCodes: CommonCode[]; |
|
| 12 | 14 |
} |
| 13 | 15 |
|
| 14 | 16 |
/** |
... | ... | @@ -22,7 +24,7 @@ |
| 22 | 24 |
* 어느 행의 버튼인지는 각 버튼 라벨이 말한다 — 감싼 요소의 `aria-label`은 의미 없는 |
| 23 | 25 |
* 요소에 붙어 읽히지 않을 수 있다(공통코드 행과 같은 방식). |
| 24 | 26 |
*/ |
| 25 |
-export function MerchantRowActions({ merchant }: MerchantRowActionsProps) {
|
|
| 27 |
+export function MerchantRowActions({ merchant, businessCodes }: MerchantRowActionsProps) {
|
|
| 26 | 28 |
const { showToast } = useFeedback();
|
| 27 | 29 |
const [isDetailOpen, setIsDetailOpen] = useState(false); |
| 28 | 30 |
|
... | ... | @@ -53,6 +55,7 @@ |
| 53 | 55 |
{isDetailOpen && (
|
| 54 | 56 |
<MerchantFormModal |
| 55 | 57 |
merchant={merchant}
|
| 58 |
+ businessCodes={businessCodes}
|
|
| 56 | 59 |
onClose={() => setIsDetailOpen(false)}
|
| 57 | 60 |
/> |
| 58 | 61 |
)} |
--- app/(protected)/(basic)/system/merchants/excel/route.ts
+++ app/(protected)/(basic)/system/merchants/excel/route.ts
... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 |
import { verifySession } from '@/lib/auth/dal';
|
| 2 |
+import { fetchCommonCodes } from '@/lib/data/repositories/common-code-repository';
|
|
| 2 | 3 |
import { fetchAllMerchants } from '@/lib/data/repositories/merchant-repository';
|
| 4 |
+import { CODE_GROUP } from '@/lib/domain/common-code';
|
|
| 3 | 5 |
import { formatCategoryLabel, formatVisibilityLabel } from '@/lib/domain/merchant';
|
| 4 | 6 |
|
| 5 | 7 |
/** |
... | ... | @@ -34,7 +36,10 @@ |
| 34 | 36 |
export async function GET() {
|
| 35 | 37 |
await verifySession(); |
| 36 | 38 |
|
| 37 |
- const merchants = await fetchAllMerchants(); |
|
| 39 |
+ const [merchants, businessCodes] = await Promise.all([ |
|
| 40 |
+ fetchAllMerchants(), |
|
| 41 |
+ fetchCommonCodes(CODE_GROUP.merchantBusiness), |
|
| 42 |
+ ]); |
|
| 38 | 43 |
|
| 39 | 44 |
const rows = merchants.map((merchant, index) => |
| 40 | 45 |
[ |
... | ... | @@ -42,7 +47,7 @@ |
| 42 | 47 |
merchant.code, |
| 43 | 48 |
merchant.brandName, |
| 44 | 49 |
merchant.name, |
| 45 |
- formatCategoryLabel(merchant.categoryCode), |
|
| 50 |
+ formatCategoryLabel(businessCodes, merchant.categoryCode), |
|
| 46 | 51 |
merchant.businessNumber, |
| 47 | 52 |
merchant.ownerName, |
| 48 | 53 |
merchant.phoneNumber, |
--- app/(protected)/(basic)/system/merchants/page.tsx
+++ app/(protected)/(basic)/system/merchants/page.tsx
... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 |
import type { Metadata } from 'next';
|
| 2 | 2 |
import { verifySession } from '@/lib/auth/dal';
|
| 3 |
+import { fetchCommonCodes } from '@/lib/data/repositories/common-code-repository';
|
|
| 3 | 4 |
import { fetchMerchants } from '@/lib/data/repositories/merchant-repository';
|
| 5 |
+import { CODE_GROUP } from '@/lib/domain/common-code';
|
|
| 4 | 6 |
import { parseMerchantQuery } from '@/lib/domain/merchant-query';
|
| 5 | 7 |
import { MerchantList } from './_components/merchant-list';
|
| 6 | 8 |
|
... | ... | @@ -18,7 +20,10 @@ |
| 18 | 20 |
await verifySession(); |
| 19 | 21 |
|
| 20 | 22 |
const query = parseMerchantQuery(await searchParams); |
| 21 |
- const { items, totalCount } = await fetchMerchants(query);
|
|
| 23 |
+ const [{ items, totalCount }, businessCodes] = await Promise.all([
|
|
| 24 |
+ fetchMerchants(query), |
|
| 25 |
+ fetchCommonCodes(CODE_GROUP.merchantBusiness), |
|
| 26 |
+ ]); |
|
| 22 | 27 |
|
| 23 | 28 |
const totalPages = Math.max(1, Math.ceil(totalCount / query.pageSize)); |
| 24 | 29 |
const currentPage = Math.min(query.page, totalPages); |
... | ... | @@ -26,6 +31,7 @@ |
| 26 | 31 |
return ( |
| 27 | 32 |
<MerchantList |
| 28 | 33 |
items={items}
|
| 34 |
+ businessCodes={businessCodes}
|
|
| 29 | 35 |
query={query}
|
| 30 | 36 |
currentPage={currentPage}
|
| 31 | 37 |
totalPages={totalPages}
|
--- lib/data/repositories/merchant-repository.ts
+++ lib/data/repositories/merchant-repository.ts
... | ... | @@ -72,7 +72,8 @@ |
| 72 | 72 |
createdAt: readString(raw, 'frstRegDtStr'), |
| 73 | 73 |
updatedAt: readString(raw, 'lastMdfcnDtStr'), |
| 74 | 74 |
name: readString(raw, 'frcsNm'), |
| 75 |
- categoryCode: readString(raw, MERCHANT_CATEGORY_READ_FIELD), |
|
| 75 |
+ // BizPlay 값이 CHAR 컬럼이라 뒤에 공백이 붙어 온다('52 ').
|
|
| 76 |
+ categoryCode: readString(raw, MERCHANT_CATEGORY_READ_FIELD).trim(), |
|
| 76 | 77 |
businessNumber: readString(raw, 'brno'), |
| 77 | 78 |
ownerName: readString(raw, 'rprsvNm'), |
| 78 | 79 |
phoneNumber: readString(raw, 'rprsvTelno'), |
--- lib/domain/common-code.ts
+++ lib/domain/common-code.ts
... | ... | @@ -41,6 +41,8 @@ |
| 41 | 41 |
}, |
| 42 | 42 |
/** 가맹점 상세의 카테고리 — 백엔드 `MngrVocDetailMapper`가 이 그룹과 조인한다. */ |
| 43 | 43 |
merchantType: 'COM_FRCS_TYPE_CD', |
| 44 |
+ /** 가맹점 업종 — BizPlay `FRCS_TPBIZ_CD` 값의 이름은 이 그룹에서 찾는다(사용자 확정). */ |
|
| 45 |
+ merchantBusiness: 'FRCS_TPBIZ_CD', |
|
| 44 | 46 |
} as const; |
| 45 | 47 |
|
| 46 | 48 |
/** 값이 없는 항목의 화면 표기. */ |
--- lib/domain/merchant.ts
+++ lib/domain/merchant.ts
... | ... | @@ -1,3 +1,4 @@ |
| 1 |
+import { formatCommonCode, type CommonCode } from '@/lib/domain/common-code';
|
|
| 1 | 2 |
/** |
| 2 | 3 |
* 백엔드 `TB_COM_VOUCHER`(`/api/v1/mngr/voc/**`) 한 행. 필드 이름이 화면 용어와 달라 |
| 3 | 4 |
* Repository가 옮긴다 — `frcsNo`=코드, `frcsNm`=가맹점명, `brno`=사업자번호, |
... | ... | @@ -39,32 +40,12 @@ |
| 39 | 40 |
: String(value); |
| 40 | 41 |
} |
| 41 | 42 |
|
| 42 |
-/** |
|
| 43 |
- * 업종 — 기획 MCH_002_p ②. |
|
| 44 |
- * |
|
| 45 |
- * TODO(기획): "제공받을 수 없는 데이터라면 추후 삭제 필요"로 적혀 있다. 코드표가 정해지면 |
|
| 46 |
- * 공통코드 조회로 바꾼다. |
|
| 47 |
- */ |
|
| 48 |
-export const MERCHANT_CATEGORY_OPTIONS: ReadonlyArray<{
|
|
| 49 |
- value: string; |
|
| 50 |
- label: string; |
|
| 51 |
-}> = [ |
|
| 52 |
- { value: 'CONVENIENCE', label: '편의점' },
|
|
| 53 |
- { value: 'CAFE', label: '카페' },
|
|
| 54 |
- { value: 'RESTAURANT', label: '음식점' },
|
|
| 55 |
- { value: 'BOOKSTORE', label: '서점' },
|
|
| 56 |
- { value: 'STATIONERY', label: '문구점' },
|
|
| 57 |
- { value: 'ETC', label: '기타' },
|
|
| 58 |
-]; |
|
| 59 |
- |
|
| 60 |
-export function formatCategoryLabel(code: string): string {
|
|
| 43 |
+/** 업종 이름 — 공통코드 `FRCS_TPBIZ_CD`에서 찾고, 없으면 코드를 그대로 보인다. */ |
|
| 44 |
+export function formatCategoryLabel(codes: readonly CommonCode[], code: string): string {
|
|
| 61 | 45 |
if (!code) {
|
| 62 | 46 |
return EMPTY_FIELD_PLACEHOLDER; |
| 63 | 47 |
} |
| 64 |
- return ( |
|
| 65 |
- MERCHANT_CATEGORY_OPTIONS.find((option) => option.value === code)?.label ?? |
|
| 66 |
- code |
|
| 67 |
- ); |
|
| 48 |
+ return formatCommonCode(codes, code); |
|
| 68 | 49 |
} |
| 69 | 50 |
|
| 70 | 51 |
export function formatVisibilityLabel(isVisible: boolean | null): string {
|
... | ... | @@ -80,10 +61,10 @@ |
| 80 | 61 |
} |
| 81 | 62 |
|
| 82 | 63 |
/** |
| 83 |
- * 업종 코드 ↔ 백엔드 필드. |
|
| 64 |
+ * 업종 코드 ↔ 백엔드 필드 — 조회·저장 모두 `FRCS_TPBIZ_CD`(BizPlay 업종코드)다. |
|
| 65 |
+ * `FRCS_DV_CD`는 가맹점 구분(온라인/일반)이라 업종이 아니다. |
|
| 84 | 66 |
* |
| 85 |
- * 이슈: 백엔드가 저장은 `FRCS_TPBIZ_CD`로 받고 조회·검색은 `FRCS_DV_CD`로 한다. 두 컬럼을 |
|
| 86 |
- * 모두 select하므로 화면은 조회 쪽(`frcsDvCd`)을 읽고, 저장할 때는 요청 VO에 있는 이름을 쓴다. |
|
| 67 |
+ * 이슈: 목록 검색만 `searchFrcsTpbizCd`를 `FRCS_DV_CD`와 비교한다(보고함). |
|
| 87 | 68 |
*/ |
| 88 |
-export const MERCHANT_CATEGORY_READ_FIELD = 'frcsDvCd'; |
|
| 69 |
+export const MERCHANT_CATEGORY_READ_FIELD = 'frcsTpbizCd'; |
|
| 89 | 70 |
export const MERCHANT_CATEGORY_WRITE_FIELD = 'frcsTpbizCd'; |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?