임동욱 임동욱 08-27
feat: 가맹점 수정을 조회 전용 모달로 연다
지금까지는 수정 버튼이 "백엔드 준비 중" 토스트만 띄워 등록된 내용을 볼 방법이
없었다. 모달을 열되 값은 바꿀 수 없게 한다.

`IS_EDIT_ENABLED` 한 줄에 전부 걸어 뒀다 — 수정 API가 생기면 그것만 true로 바꾸면
입력칸도 저장 버튼도 그대로 살아난다. 폼 로직은 손대지 않았다.

Co-Authored-By: Claude Opus 5 
@391793840e1b86b19e537f49025651a2096cc3ac
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
@@ -7,6 +7,7 @@
   useState,
   useTransition,
 } from 'react';
+import { FoxAlert } from '@fox/core/components/fox-alert';
 import { FoxButton } from '@fox/core/components/fox-button';
 import { FoxFormLabel } from '@fox/core/components/fox-form-label';
 import { FoxHelperText } from '@fox/core/components/fox-helper-text';
@@ -34,6 +35,13 @@
 import { openPostcodeSearch } from './postcode';
 import styles from './merchant-form.module.scss';
 
+/**
+ * 백엔드에 가맹점 수정 API가 없어(`/api/v1/mngr/voc`는 목록·등록뿐) 수정 모달을 조회
+ * 전용으로 연다. **API가 생기면 이 한 줄만 `true`로 바꾸면 된다** — 입력칸·저장 버튼이
+ * 전부 이 값에 걸려 있고 폼 자체는 그대로 살아 있다.
+ */
+const IS_EDIT_ENABLED = false;
+
 interface MerchantFormModalProps {
   merchant?: Merchant;
   onClose: () => void;
@@ -41,6 +49,10 @@
 
 export function MerchantFormModal({ merchant, onClose }: MerchantFormModalProps) {
   const isEdit = Boolean(merchant);
+  const isReadOnly = isEdit && !IS_EDIT_ENABLED;
+  // 조회 전용일 때 입력칸마다 얹는다. `state="view"`는 시안의 조회 상태로, 잠긴 것(disabled)과
+  // 달리 "볼 수만 있는 값"으로 보이게 한다.
+  const viewProps = isReadOnly ? ({ readOnly: true, state: 'view' } as const) : null;
   const { showToast } = useFeedback();
   const formRef = useRef<HTMLFormElement>(null);
   const [state, formAction, isPending] = useActionState(
@@ -105,19 +117,23 @@
     <FoxModal
       open
       size="md"
-      title={isEdit ? '가맹점 수정' : '가맹점 등록'}
+      title={isReadOnly ? '가맹점 정보' : isEdit ? '가맹점 수정' : '가맹점 등록'}
       onClose={onClose}
       actions={
-        <>
-          <FoxButton type="default" size="md" label="취소" onAction={onClose} />
-          <FoxButton
-            type="primary"
-            size="md"
-            label={isPending ? '저장 중...' : isEdit ? '수정' : '등록'}
-            disabled={isPending}
-            onAction={() => formRef.current?.requestSubmit()}
-          />
-        </>
+        isReadOnly ? (
+          <FoxButton type="default" size="md" label="닫기" onAction={onClose} />
+        ) : (
+          <>
+            <FoxButton type="default" size="md" label="취소" onAction={onClose} />
+            <FoxButton
+              type="primary"
+              size="md"
+              label={isPending ? '저장 중...' : isEdit ? '수정' : '등록'}
+              disabled={isPending}
+              onAction={() => formRef.current?.requestSubmit()}
+            />
+          </>
+        )
       }
     >
       <form
@@ -125,6 +141,13 @@
         onSubmit={(event) => submitFormAction(event, formAction)}
         className={styles.fields}
       >
+        {isReadOnly && (
+          <FoxAlert
+            type="information"
+            message="가맹점 수정은 백엔드 준비 중입니다. 지금은 등록된 내용만 확인할 수 있습니다."
+          />
+        )}
+
         <input type="hidden" name="id" value={merchant?.id ?? ''} />
         <input type="hidden" name="categoryCode" value={categoryCode} />
         <input type="hidden" name="isVisible" value={isVisible ? 'Y' : 'N'} />
@@ -150,6 +173,7 @@
             항목이라 화면에서는 지우지 않고, 값이 없으면 `-`로 보여준다(사용자 확정). */}
         <div className={styles.field}>
           <FoxInput
+            {...viewProps}
             size="md"
             name="brandName"
             label="브랜드명"
@@ -161,6 +185,7 @@
 
         <div className={styles.field}>
           <FoxInput
+            {...viewProps}
             size="md"
             name="name"
             label="가맹점명"
@@ -175,6 +200,7 @@
 
         <div className={styles.field}>
           <FoxSelect
+            readOnly={isReadOnly}
             size="md"
             label="업종"
             requirement="required"
@@ -193,6 +219,7 @@
         <div className={styles.field}>
           <div className={styles.pair}>
             <FoxInput
+              {...viewProps}
               size="md"
               name="businessNumber"
               label="사업자등록번호"
@@ -202,6 +229,7 @@
               message={errors.businessNumber}
             />
             <FoxInput
+              {...viewProps}
               size="md"
               name="ownerName"
               label="대표자명"
@@ -214,6 +242,7 @@
 
         <div className={styles.field}>
           <FoxInput
+            {...viewProps}
             size="md"
             name="phoneNumber"
             label="전화번호"
@@ -238,15 +267,18 @@
               state="view"
               placeholder="주소 검색을 진행해주세요."
             />
-            <FoxButton
-              type="secondary"
-              size="md"
-              label={isSearching ? '검색 중...' : '주소 검색'}
-              disabled={isSearching}
-              onAction={searchAddress}
-            />
+            {!isReadOnly && (
+              <FoxButton
+                type="secondary"
+                size="md"
+                label={isSearching ? '검색 중...' : '주소 검색'}
+                disabled={isSearching}
+                onAction={searchAddress}
+              />
+            )}
           </div>
           <FoxInput
+            {...viewProps}
             size="md"
             name="addressDetail"
             aria-label="상세주소"
@@ -287,12 +319,14 @@
           </FoxFormLabel>
           <FoxRadioGroup name="visibility" size="md" label="사용여부">
             <FoxRadio
+              disabled={isReadOnly}
               value="Y"
               label="노출"
               checked={isVisible}
               onChange={() => setIsVisible(true)}
             />
             <FoxRadio
+              disabled={isReadOnly}
               value="N"
               label="미노출"
               checked={!isVisible}
@@ -303,6 +337,7 @@
 
         <div className={styles.field}>
           <FoxInput
+            {...viewProps}
             size="md"
             name="memo"
             label="메모"
app/(protected)/(basic)/system/merchants/_components/merchant-row-actions.tsx
--- app/(protected)/(basic)/system/merchants/_components/merchant-row-actions.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-row-actions.tsx
@@ -1,26 +1,26 @@
 'use client';
 
+import { useState } from 'react';
 import { FoxIconButton } from '@fox/core/components/fox-icon-button';
 import { useFeedback } from '@/app/_hooks/use-feedback';
 import type { Merchant } from '@/lib/domain/merchant';
+import { MerchantFormModal } from './merchant-form-modal';
 
 interface MerchantRowActionsProps {
   merchant: Merchant;
 }
 
 /**
- * TODO(백엔드): 수정·삭제 API가 없다 — `/api/v1/mngr/voc`는 목록(`/pagination`)과 등록뿐이다.
- * 시안의 자리는 두되 눌러도 할 수 있는 일이 없어, 비활성 대신 사유를 알리도록 했다.
+ * 수정은 모달을 열어 등록된 내용을 보여 준다 — 백엔드에 수정 API가 없어 모달이 조회
+ * 전용으로 뜬다(`merchant-form-modal.tsx`의 `IS_EDIT_ENABLED`). API가 생기면 그 한 줄만
+ * 바꾸면 여기는 손대지 않아도 된다.
+ *
+ * TODO(백엔드): 삭제 API도 없다(`/api/v1/mngr/voc`는 목록·등록뿐). 시안의 자리는 두되
+ * 눌러도 할 수 있는 일이 없어 사유를 알린다.
  */
 export function MerchantRowActions({ merchant }: MerchantRowActionsProps) {
   const { showToast } = useFeedback();
-
-  function notReady() {
-    showToast({
-      variant: 'warning',
-      message: '가맹점 수정·삭제 기능은 백엔드 준비 중입니다.',
-    });
-  }
+  const [isDetailOpen, setIsDetailOpen] = useState(false);
 
   return (
     <span aria-label={`${merchant.name} 관리`}>
@@ -29,15 +29,27 @@
         size="sm"
         icon={<i className="fox-ico fox-ico-PencilSimple" aria-hidden="true" />}
         label="수정"
-        onAction={notReady}
+        onAction={() => setIsDetailOpen(true)}
       />
       <FoxIconButton
         type="default"
         size="sm"
         icon={<i className="fox-ico fox-ico-Trash" aria-hidden="true" />}
         label="삭제"
-        onAction={notReady}
+        onAction={() =>
+          showToast({
+            variant: 'warning',
+            message: '가맹점 삭제 기능은 백엔드 준비 중입니다.',
+          })
+        }
       />
+
+      {isDetailOpen && (
+        <MerchantFormModal
+          merchant={merchant}
+          onClose={() => setIsDetailOpen(false)}
+        />
+      )}
     </span>
   );
 }
Add a comment
List