임동욱 임동욱 08-21
fix: 가맹점에서 백엔드에 없는 항목을 화면에 되돌린다
기획 시안과 틀어져 지우면 안 되는 항목이었다(사용자 확정). 브랜드명·메모·등록일·
최종수정일을 목록·팝업·엑셀에 되살리고, 값이 없을 때 '-'가 나오게 한다.

등록 시 브랜드명·메모도 함께 보낸다 — 요청 VO에 없어 지금은 버려지지만 컬럼이
생기면 프론트 수정 없이 저장된다.

Co-Authored-By: Claude Opus 5 
@67d58df61ead1812f955514f4cca4f20f7ed34e7
app/(protected)/(basic)/system/merchants/_actions.ts
--- app/(protected)/(basic)/system/merchants/_actions.ts
+++ app/(protected)/(basic)/system/merchants/_actions.ts
@@ -24,6 +24,8 @@
 
   const validation = validateMerchant({
     id: readString(formData, 'id'),
+    brandName: readString(formData, 'brandName'),
+    memo: readString(formData, 'memo'),
     name: readString(formData, 'name'),
     categoryCode: readString(formData, 'categoryCode'),
     businessNumber: readString(formData, 'businessNumber'),
app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
--- app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
@@ -13,11 +13,14 @@
 import { useFeedback } from '@/app/_hooks/use-feedback';
 import {
   MERCHANT_CATEGORY_OPTIONS,
+  formatOptionalValue,
   type Merchant,
 } from '@/lib/domain/merchant';
 import {
   ADDRESS_DETAIL_MAX_LENGTH,
+  BRAND_MAX_LENGTH,
   INITIAL_MERCHANT_FORM_STATE,
+  MEMO_MAX_LENGTH,
   NAME_MAX_LENGTH,
   OWNER_MAX_LENGTH,
 } from '@/lib/domain/merchant-form';
@@ -114,6 +117,19 @@
             />
           </div>
         )}
+
+        {/* 브랜드명·메모·등록일은 백엔드에 컬럼이 없어 저장되지 않는다. 기획 시안에 있는
+            항목이라 화면에서는 지우지 않고, 값이 없으면 `-`로 보여준다(사용자 확정). */}
+        <div className={styles.field}>
+          <FoxInput
+            size="md"
+            name="brandName"
+            label="브랜드명"
+            defaultValue={merchant?.brandName ?? ''}
+            placeholder="브랜드명을 입력하세요."
+            maxLength={BRAND_MAX_LENGTH}
+          />
+        </div>
 
         <div className={styles.field}>
           <FoxInput
@@ -256,6 +272,39 @@
           </FoxRadioGroup>
         </div>
 
+        <div className={styles.field}>
+          <FoxInput
+            size="md"
+            name="memo"
+            label="메모"
+            defaultValue={merchant?.memo ?? ''}
+            placeholder="관리자 메모 (사용자에게 노출되지 않음)"
+            maxLength={MEMO_MAX_LENGTH}
+          />
+        </div>
+
+        {isEdit && (
+          <div className={styles.field}>
+            <FoxFormLabel as="span">등록일 / 최종수정일</FoxFormLabel>
+            <div className={styles.pair}>
+              <FoxInput
+                size="md"
+                aria-label="등록일"
+                value={formatOptionalValue(merchant?.createdAt)}
+                readOnly
+                state="view"
+              />
+              <FoxInput
+                size="md"
+                aria-label="최종수정일"
+                value={formatOptionalValue(merchant?.updatedAt)}
+                readOnly
+                state="view"
+              />
+            </div>
+          </div>
+        )}
+
         {state.status === 'error' && state.message && (
           <FoxHelperText type="danger" message={state.message} />
         )}
app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
--- app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
@@ -66,6 +66,12 @@
       render: (_row, index) => (currentPage - 1) * query.pageSize + index + 1,
     },
     { key: 'code', header: '가맹점코드', width: 140 },
+    {
+      key: 'brandName',
+      header: '브랜드명',
+      width: 120,
+      render: (row) => formatOptionalValue(row.brandName),
+    },
     { key: 'name', header: '가맹점명', width: 240, emphasis: true },
     {
       key: 'categoryCode',
@@ -97,6 +103,12 @@
       ),
     },
     {
+      key: 'createdAt',
+      header: '등록일',
+      width: 140,
+      render: (row) => formatOptionalValue(row.createdAt),
+    },
+    {
       key: 'actions',
       header: '관리',
       width: 120,
app/(protected)/(basic)/system/merchants/excel/route.ts
--- app/(protected)/(basic)/system/merchants/excel/route.ts
+++ app/(protected)/(basic)/system/merchants/excel/route.ts
@@ -12,6 +12,7 @@
 const HEADERS = [
   '번호',
   '가맹점코드',
+  '브랜드명',
   '가맹점명',
   '업종',
   '사업자등록번호',
@@ -21,6 +22,8 @@
   '상세주소',
   '우편번호',
   '사용여부',
+  '메모',
+  '등록일',
 ];
 
 /** 쉼표·따옴표·줄바꿈이 든 값은 따옴표로 감싸고 내부 따옴표는 두 번 쓴다(RFC 4180). */
@@ -37,6 +40,7 @@
     [
       String(index + 1),
       merchant.code,
+      merchant.brandName,
       merchant.name,
       formatCategoryLabel(merchant.categoryCode),
       merchant.businessNumber,
@@ -46,6 +50,8 @@
       merchant.addressDetail,
       merchant.postalCode,
       formatVisibilityLabel(merchant.isVisible),
+      merchant.memo,
+      merchant.createdAt,
     ]
       .map(toCsvCell)
       .join(',')
lib/data/repositories/merchant-repository.ts
--- lib/data/repositories/merchant-repository.ts
+++ lib/data/repositories/merchant-repository.ts
@@ -27,7 +27,8 @@
  *    뒤집어 오래된 가맹점부터 나온다.
  * 5. `USE_YN`을 select하지 않는다 — 검색 조건에는 쓰면서 응답에는 없어 사용여부를 알 수 없다.
  *    화면은 노출로 보인다.
- * 6. 브랜드명·메모·등록일 컬럼이 없다 — 기획에 있으나 담을 자리가 없어 화면에서 뺐다.
+ * 6. 브랜드명·메모·등록일·최종수정일 컬럼이 없다 — 기획에 있는 항목이라 화면은 그대로 두고
+ *    값만 비워 `-`로 보인다. 등록 시 보내는 브랜드명·메모도 요청 VO에 없어 버려진다.
  */
 
 const VOC_PATH = '/api/v1/mngr/voc';
@@ -65,6 +66,11 @@
     {
       id: code,
       code,
+      // 백엔드에 컬럼이 없다(⚠️ 6) — 응답에 있으면 쓰고, 없으면 빈 값으로 두어 화면이 `-`를 낸다.
+      brandName: readString(raw, 'brandNm'),
+      memo: readString(raw, 'memo'),
+      createdAt: readString(raw, 'frstRegDtStr'),
+      updatedAt: readString(raw, 'lastMdfcnDtStr'),
       name: readString(raw, 'frcsNm'),
       categoryCode: readString(raw, MERCHANT_CATEGORY_READ_FIELD),
       businessNumber: readString(raw, 'brno'),
@@ -167,6 +173,9 @@
       lat: values.latitude,
       lng: values.longitude,
       useYn: values.isVisible ? 'Y' : 'N',
+      // 요청 VO에 없어 무시된다. 컬럼이 생기면 그대로 저장되도록 미리 싣는다.
+      brandNm: values.brandName,
+      memo: values.memo,
     },
     accessToken: accessToken ?? undefined,
     cache: 'no-store',
lib/domain/merchant-form.ts
--- lib/domain/merchant-form.ts
+++ lib/domain/merchant-form.ts
@@ -1,10 +1,14 @@
-// 이슈: 브랜드명·메모는 백엔드 TB_COM_VOUCHER에 컬럼이 없어 화면에서 뺐다(기획에는 있다).
 export const NAME_MAX_LENGTH = 100;
+export const BRAND_MAX_LENGTH = 100;
 export const OWNER_MAX_LENGTH = 50;
 export const ADDRESS_DETAIL_MAX_LENGTH = 200;
+export const MEMO_MAX_LENGTH = 500;
 
 export interface MerchantValues {
   id: string;
+  /** 백엔드에 컬럼이 없어 저장되지 않는다 — 기획 시안 유지를 위해 화면에는 남긴다. */
+  brandName: string;
+  memo: string;
   name: string;
   categoryCode: string;
   businessNumber: string;
@@ -77,6 +81,8 @@
     values: {
       ...values,
       name,
+      brandName: values.brandName.trim().slice(0, BRAND_MAX_LENGTH),
+      memo: values.memo.trim().slice(0, MEMO_MAX_LENGTH),
       ownerName: values.ownerName.trim().slice(0, OWNER_MAX_LENGTH),
       phoneNumber,
       businessNumber,
lib/domain/merchant.ts
--- lib/domain/merchant.ts
+++ lib/domain/merchant.ts
@@ -7,6 +7,14 @@
   id: string;
   /** `frcsNo` — 서버가 만든다. 수정 불가(기획 MCH_003_p ①). */
   code: string;
+  /**
+   * 아래 넷은 백엔드 `TB_COM_VOUCHER`에 컬럼이 없어 **항상 빈 값**이다. 기획 시안에 있는
+   * 항목이라 화면에서는 지우지 않고 값이 없을 때 `-`로 보여준다(사용자 확정).
+   */
+  brandName: string;
+  memo: string;
+  createdAt: string;
+  updatedAt: string;
   name: string;
   categoryCode: string;
   businessNumber: string;
Add a comment
List