feat: 가맹몰 상세 등록 직후 같은 화면에 남아 바로 메뉴를 추가하게 한다
/api/v1/mngr/voc/product 계약을 origin/develop 22f67cb로 재확인했다 — 등록은 가맹점만 있으면 받지만 목록·단건·수정·삭제는 상세와 INNER JOIN이라 상세 저장이 먼저여야 한다. 등록 저장 뒤 목록으로 나가면 [수정]을 다시 눌러야 메뉴를 붙일 수 있었으므로, 등록만 같은 화면(수정 모드)에 남긴다. 목록 SQL에 ORDER BY가 없어 노출 순서 오름차순으로 정렬한다. Co-Authored-By: Claude Opus 5
@83801e9dff362ffc18c2e50f1c15ab8754e37ec8
--- app/(protected)/(basic)/system/merchant-details/_actions.ts
+++ app/(protected)/(basic)/system/merchant-details/_actions.ts
... | ... | @@ -257,7 +257,10 @@ |
| 257 | 257 |
|
| 258 | 258 |
revalidatePath(MERCHANT_DETAILS_PATH); |
| 259 | 259 |
revalidatePath(merchantDetailPath(frcsNo)); |
| 260 |
- redirect(MERCHANT_DETAILS_PATH); |
|
| 260 |
+ // 등록 직후엔 같은 화면(수정 모드)에 남긴다 — 메뉴는 상세 행이 있어야 등록·조회되므로 |
|
| 261 |
+ // (백엔드 목록 SQL이 상세와 INNER JOIN) 여기서 바로 [+ 메뉴 추가]로 이어지게 한다. |
|
| 262 |
+ // 수정은 다른 화면처럼 목록으로 돌아간다. |
|
| 263 |
+ redirect(isEdit ? MERCHANT_DETAILS_PATH : merchantDetailPath(frcsNo)); |
|
| 261 | 264 |
} |
| 262 | 265 |
|
| 263 | 266 |
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
... | ... | @@ -47,7 +47,9 @@ |
| 47 | 47 |
/> |
| 48 | 48 |
</div> |
| 49 | 49 |
{!canAdd && (
|
| 50 |
- <p className={styles.hint}>상세를 먼저 저장하면 메뉴를 추가할 수 있습니다.</p>
|
|
| 50 |
+ <p className={styles.hint}>
|
|
| 51 |
+ 아래 [등록]으로 상세를 먼저 저장하면 이 화면에서 바로 메뉴를 추가할 수 있습니다. |
|
| 52 |
+ </p> |
|
| 51 | 53 |
)} |
| 52 | 54 |
|
| 53 | 55 |
<div className={styles.table}>
|
--- lib/data/repositories/merchant-product-repository.ts
+++ lib/data/repositories/merchant-product-repository.ts
... | ... | @@ -10,15 +10,19 @@ |
| 10 | 10 |
import type { MerchantProductValues } from '@/lib/domain/merchant-product-form';
|
| 11 | 11 |
|
| 12 | 12 |
/** |
| 13 |
- * 가맹몰 메뉴 Repository — edupay-backend origin/develop 15d409a 기준. |
|
| 13 |
+ * 가맹몰 메뉴 Repository — edupay-backend origin/develop 22f67cb 기준(2026-09-14 재확인). |
|
| 14 | 14 |
* |
| 15 | 15 |
* ``` |
| 16 |
- * GET /api/v1/mngr/voc/product/list/{frcsNo} 상세가 있는 가맹점의 메뉴 전체(페이징 없음)
|
|
| 17 |
- * GET /api/v1/mngr/voc/product/{frcsProductSn}
|
|
| 18 |
- * POST /api/v1/mngr/voc/product @RequestBody · 가맹점 상세가 있어야 한다 |
|
| 19 |
- * PUT /api/v1/mngr/voc/product/{frcsProductSn} @RequestBody
|
|
| 20 |
- * DELETE /api/v1/mngr/voc/product/{frcsProductSn} 논리 삭제
|
|
| 16 |
+ * GET /api/v1/mngr/voc/product/list/{frcsNo} 메뉴 전체(페이징 없음) — 상세와 INNER JOIN
|
|
| 17 |
+ * GET /api/v1/mngr/voc/product/{frcsProductSn} 없으면 NOT_FOUND 봉투
|
|
| 18 |
+ * POST /api/v1/mngr/voc/product @RequestBody · 가맹점만 있으면 받는다 |
|
| 19 |
+ * PUT /api/v1/mngr/voc/product/{frcsProductSn} @RequestBody · 전 컬럼 덮어씀
|
|
| 20 |
+ * DELETE /api/v1/mngr/voc/product/{frcsProductSn} 논리 삭제(DEL_YN)
|
|
| 21 | 21 |
* ``` |
| 22 |
+ * |
|
| 23 |
+ * 등록은 상세 행이 없어도 통과하지만 목록·단건·수정·삭제는 상세와 INNER JOIN이라 상세를 저장하기 |
|
| 24 |
+ * 전에 넣은 메뉴는 보이지도 고쳐지지도 않는다 — 그래서 화면은 상세를 먼저 저장하게 한다. |
|
| 25 |
+ * 목록 SQL에 ORDER BY가 없어 노출 순서(낮을수록 상단)는 여기서 정렬한다. |
|
| 22 | 26 |
*/ |
| 23 | 27 |
|
| 24 | 28 |
const PRODUCT_PATH = '/api/v1/mngr/voc/product'; |
... | ... | @@ -82,7 +86,16 @@ |
| 82 | 86 |
if (!result.ok) {
|
| 83 | 87 |
throw new BackendRequestError(result); |
| 84 | 88 |
} |
| 85 |
- return Array.isArray(result.data) ? result.data.flatMap(toProduct) : []; |
|
| 89 |
+ const products = Array.isArray(result.data) ? result.data.flatMap(toProduct) : []; |
|
| 90 |
+ return products.sort(compareProductOrder); |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+/** 노출 순서 오름차순, 순서가 없는 메뉴는 뒤로, 같으면 등록된 순(번호)대로. */ |
|
| 94 |
+function compareProductOrder(a: MerchantProduct, b: MerchantProduct): number {
|
|
| 95 |
+ const orderA = a.sortOrder ?? Number.POSITIVE_INFINITY; |
|
| 96 |
+ const orderB = b.sortOrder ?? Number.POSITIVE_INFINITY; |
|
| 97 |
+ if (orderA !== orderB) return orderA - orderB; |
|
| 98 |
+ return a.id.localeCompare(b.id, undefined, { numeric: true });
|
|
| 86 | 99 |
} |
| 87 | 100 |
|
| 88 | 101 |
export async function fetchMerchantProduct( |
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?