임동욱 임동욱 09-16
feat: 가맹몰 상세 등록과 수정을 한 화면으로 합친다
제목·경로 표시·구성을 「가맹몰 상세 정보」 하나로 두고, 저장 액션은 리다이렉트 대신 결과를
돌려준다. 처음 저장하면 토스트를 띄우고 그 가맹점 주소로만 옮겨 가 화면은 그대로이고
(바로 메뉴 추가 가능), 이후 저장은 그 자리에서 새로고침한다(사용자 확정). 이미지 칸은
저장으로 파일 id가 바뀌면 다시 마운트해 서버 값과 맞춘다.

Co-Authored-By: Claude Opus 5 
@f27562e22940af8805d47b6b849f3a6baafa7e42
app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
--- app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
+++ app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
@@ -1,7 +1,7 @@
 'use client';
 
 import { useRouter } from 'next/navigation';
-import { useActionState, useState } from 'react';
+import { useActionState, useEffect, useState } from 'react';
 import { FoxButton } from '@fox/core/components/fox-button';
 import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
 import { FoxButtonPanel } from '@fox/core/components/fox-button-panel';
@@ -29,7 +29,7 @@
   INITIAL_MERCHANT_DETAIL_FORM_STATE,
   SORT_ORDER_MAX,
 } from '@/lib/domain/merchant-detail-form';
-import { MERCHANT_DETAILS_PATH } from '@/lib/domain/merchant-detail-query';
+import { MERCHANT_DETAILS_PATH, merchantDetailPath } from '@/lib/domain/merchant-detail-query';
 import type { MerchantProduct } from '@/lib/domain/merchant-product';
 import { formatPhoneNumber } from '@/lib/domain/phone-number';
 import {
@@ -54,11 +54,13 @@
 }
 
 /**
- * 가맹몰 상세 등록·수정 — 기획 BO-MAL-002(등록)·003(수정). `detail`이 있으면 수정, 없으면 등록이다.
+ * 가맹몰 상세 — 기획 BO-MAL-002(등록)·003(수정)을 한 화면으로 합쳤다(사용자 확정). `detail`이
+ * 있으면 상세가 이미 있는 가맹점이고, 없으면 업체명 자리에서 상세가 없는 가맹점을 골라 처음 만든다.
+ * 처음 저장하면 그 가맹점 주소로 옮겨 가되 화면 구성은 그대로라 바로 메뉴를 붙일 수 있고,
+ * 이후 저장은 그 자리에 머문다.
  *
- * 상세는 기존 가맹점에만 붙으므로 등록 화면은 업체명 자리에서 상세가 없는 가맹점을 고른다
- * (사용자 확정). 업체명·연락처·우편번호·주소는 가맹점 원본이라 읽기 전용이고, 기획의 [주소 검색]은
- * 가맹점 관리 화면의 것이라 여기서는 값을 바꾸지 않는다.
+ * 업체명·연락처·우편번호·주소는 가맹점 원본이라 읽기 전용이고, 기획의 [주소 검색]은 가맹점 관리
+ * 화면의 것이라 여기서는 값을 바꾸지 않는다.
  */
 export function MerchantDetailForm({
   detail,
@@ -86,6 +88,17 @@
     detail?.isVisible === false ? 'N' : 'Y',
   ]);
   const [voucherUse, setVoucherUse] = useState<string[]>(['Y']);
+
+  useEffect(() => {
+    if (state.status !== 'success') return;
+    if (state.created) {
+      showToast({ variant: 'success', message: '등록했습니다. 이어서 메뉴를 추가할 수 있습니다.' });
+      router.replace(merchantDetailPath(state.frcsNo));
+    } else {
+      showToast({ variant: 'success', message: '저장했습니다.' });
+      router.refresh();
+    }
+  }, [state, router, showToast]);
 
   function toggleVisibility() {
     if (!detail) return;
@@ -132,17 +145,13 @@
   return (
     <>
       <FoxPageHeader
-        title={`가맹몰 상세 ${isEdit ? '수정' : '등록'}`}
-        description={
-          isEdit
-            ? '등록된 업체 정보를 수정한다. 저장 즉시 앱에 반영된다'
-            : '지역상생몰에 노출할 업체를 등록한다'
-        }
+        title="가맹몰 상세 정보"
+        description="지역상생몰에 노출할 업체의 상세를 등록·수정한다. 저장 즉시 앱에 반영된다"
         breadcrumb={[
           { label: '홈', href: '/' },
           { label: '관리자관리' },
           { label: '가맹몰 상세 관리', href: MERCHANT_DETAILS_PATH },
-          { label: isEdit ? '수정' : '등록' },
+          { label: '상세 정보' },
         ]}
       />
 
@@ -309,7 +318,9 @@
           description="대표 1장 필수 · 추가 최대 11장 (앱 슬라이더 최대 12장)"
           className={styles.section}
         >
+          {/* 저장으로 파일 id가 바뀌면 다시 마운트해 칸 상태를 서버 값으로 되돌린다. */}
           <MerchantImageField
+            key={`${detail?.thumbnailFileId ?? ''}/${detail?.imageFileId ?? ''}`}
             defaultThumbnailUrl={detail?.thumbnailUrl ?? null}
             retainedThumbnailId={detail?.thumbnailFileId ?? null}
             defaultImageUrls={detail?.imageUrls ?? []}
app/(protected)/(basic)/system/merchant-details/[frcsNo]/page.tsx
--- app/(protected)/(basic)/system/merchant-details/[frcsNo]/page.tsx
+++ app/(protected)/(basic)/system/merchant-details/[frcsNo]/page.tsx
@@ -8,7 +8,7 @@
 import { merchantDetailNewPath } from '@/lib/domain/merchant-detail-query';
 import { MerchantDetailForm } from './_components/merchant-detail-form';
 
-export const metadata: Metadata = { title: '가맹몰 상세 수정' };
+export const metadata: Metadata = { title: '가맹몰 상세 정보' };
 export const dynamic = 'force-dynamic';
 
 interface PageProps {
app/(protected)/(basic)/system/merchant-details/_actions.ts
--- app/(protected)/(basic)/system/merchant-details/_actions.ts
+++ app/(protected)/(basic)/system/merchant-details/_actions.ts
@@ -257,10 +257,8 @@
 
   revalidatePath(MERCHANT_DETAILS_PATH);
   revalidatePath(merchantDetailPath(frcsNo));
-  // 등록 직후엔 같은 화면(수정 모드)에 남긴다 — 메뉴는 상세 행이 있어야 등록·조회되므로
-  // (백엔드 목록 SQL이 상세와 INNER JOIN) 여기서 바로 [+ 메뉴 추가]로 이어지게 한다.
-  // 수정은 다른 화면처럼 목록으로 돌아간다.
-  redirect(isEdit ? MERCHANT_DETAILS_PATH : merchantDetailPath(frcsNo));
+  // 등록·수정이 한 화면이라 여기서 옮기지 않는다 — 화면이 결과를 보고 머물거나 주소만 바꾼다.
+  return { status: 'success', frcsNo, created: !isEdit };
 }
 
 export async function deleteMerchantDetailAction(
app/(protected)/(basic)/system/merchant-details/_components/merchant-product-table.tsx
--- app/(protected)/(basic)/system/merchant-details/_components/merchant-product-table.tsx
+++ app/(protected)/(basic)/system/merchant-details/_components/merchant-product-table.tsx
@@ -48,7 +48,7 @@
       </div>
       {!canAdd && (
         <p className={styles.hint}>
-          아래 [등록]으로 상세를 먼저 저장하면 이어지는 수정 화면에서 메뉴를 추가할 수 있습니다.
+          아래 [등록]으로 상세를 먼저 저장하면 이 화면에서 바로 메뉴를 추가할 수 있습니다.
         </p>
       )}
 
app/(protected)/(basic)/system/merchant-details/new/page.tsx
--- app/(protected)/(basic)/system/merchant-details/new/page.tsx
+++ app/(protected)/(basic)/system/merchant-details/new/page.tsx
@@ -5,7 +5,7 @@
 import { CODE_GROUP } from '@/lib/domain/common-code';
 import { MerchantDetailForm } from '../[frcsNo]/_components/merchant-detail-form';
 
-export const metadata: Metadata = { title: '가맹몰 상세 등록' };
+export const metadata: Metadata = { title: '가맹몰 상세 정보' };
 export const dynamic = 'force-dynamic';
 
 interface PageProps {
lib/domain/merchant-detail-form.ts
--- lib/domain/merchant-detail-form.ts
+++ lib/domain/merchant-detail-form.ts
@@ -45,7 +45,8 @@
 export type MerchantDetailFormState =
   | { status: 'idle' }
   | { status: 'error'; message?: string; errors?: MerchantDetailFormErrors }
-  | { status: 'success' };
+  /** `created`가 참이면 이번 저장으로 상세 행이 처음 생겼다 — 화면이 그 가맹점 주소로 옮겨 간다. */
+  | { status: 'success'; frcsNo: string; created: boolean };
 
 export const INITIAL_MERCHANT_DETAIL_FORM_STATE: MerchantDetailFormState = {
   status: 'idle',
Add a comment
List