docs: 가맹몰 상세 모듈을 develop(feature/voc 병합) 계약으로 재검토하고 409 안내를 더한다
단건 조회가 상세 없음을 NOT_FOUND 대신 성공 봉투의 null로 주는 것, 삭제된 상세를 selectOne이 그대로 돌려주는 결함(재등록 409·수정 화면에 삭제값 노출), 프론트가 쓰지 않게 된 건수 쿼리 문제를 문서에 맞춘다. 등록 409는 삭제 잔존 안내 문구로 바꾼다. Co-Authored-By: Claude Fable 5.1
@b1c4712395339108756fd62cc2240b981595fbe4
--- app/(protected)/(basic)/system/merchant-details/_actions.ts
+++ app/(protected)/(basic)/system/merchant-details/_actions.ts
... | ... | @@ -51,10 +51,8 @@ |
| 51 | 51 |
/** |
| 52 | 52 |
* 가맹몰 상세 Server Action — 상세(가맹점당 1건)와 메뉴(여러 건) 둘 다 여기서 다룬다. |
| 53 | 53 |
* |
| 54 |
- * 이슈: 목록의 [비활성]/[노출]은 백엔드가 상세 전체를 돌려줘야 안전하다. `PUT`이 모든 컬럼을 |
|
| 55 |
- * 무조건 덮어써서, 단건 조회가 비어 있는 지금 값 없이 보내면 소개·영업시간·이미지가 지워진다. |
|
| 56 |
- * 그래서 조회 결과에 대표이미지가 없으면 목록 토글을 거부하고 수정 화면으로 안내한다. |
|
| 57 |
- * 단건 SELECT가 보강되면(보고함) 그대로 동작한다. |
|
| 54 |
+ * 노출 토글은 단건을 읽어 전 컬럼을 그대로 다시 보낸다 — 백엔드 `PUT`이 모든 컬럼을 덮어쓰기 때문이다. |
|
| 55 |
+ * 단건이 대표이미지·카테고리를 안 주는 응답이면(구 버전) 값이 지워지므로 토글을 거부한다. |
|
| 58 | 56 |
*/ |
| 59 | 57 |
|
| 60 | 58 |
const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.'; |
... | ... | @@ -62,6 +60,9 @@ |
| 62 | 60 |
const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.'; |
| 63 | 61 |
const DETAIL_NOT_LOADED_MESSAGE = |
| 64 | 62 |
'백엔드가 상세 값을 돌려주지 않아 목록에서는 바꿀 수 없습니다. 수정 화면에서 바꿔 주세요.'; |
| 63 |
+// 백엔드가 삭제된 상세도 "있는 것"으로 보는 결함(보고함) — 409는 대개 이 경우다. |
|
| 64 |
+const DETAIL_CONFLICT_MESSAGE = |
|
| 65 |
+ '이 가맹점에는 이미 상세가 있습니다. 삭제했던 상세라면 백엔드에서 정리해야 다시 등록할 수 있습니다.'; |
|
| 65 | 66 |
const IMAGE_OVERSIZE_MESSAGE = `이미지는 장당 ${Math.floor(MAX_IMAGE_BYTES / (1024 * 1024))}MB까지 올릴 수 있습니다.`;
|
| 66 | 67 |
const IMAGES_TOO_MANY_MESSAGE = `추가 이미지는 최대 ${MAX_EXTRA_IMAGES}장까지입니다.`;
|
| 67 | 68 |
const REQUEST_OVERSIZE_MESSAGE = `한 번에 올리는 이미지의 합이 ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))}MB를 넘습니다. 나눠서 저장해 주세요.`;
|
... | ... | @@ -277,7 +278,11 @@ |
| 277 | 278 |
} catch (error) {
|
| 278 | 279 |
unstable_rethrow(error); |
| 279 | 280 |
console.error('[merchant-details] 저장 실패', error);
|
| 280 |
- failed = { status: 'error', message: failureMessage(error, SAVE_FAILED_MESSAGE) };
|
|
| 281 |
+ const isConflict = !isEdit && error instanceof BackendRequestError && error.code === 409; |
|
| 282 |
+ failed = {
|
|
| 283 |
+ status: 'error', |
|
| 284 |
+ message: isConflict ? DETAIL_CONFLICT_MESSAGE : failureMessage(error, SAVE_FAILED_MESSAGE), |
|
| 285 |
+ }; |
|
| 281 | 286 |
} |
| 282 | 287 |
if (failed) {
|
| 283 | 288 |
return failed; |
--- lib/data/repositories/merchant-detail-repository.ts
+++ lib/data/repositories/merchant-detail-repository.ts
... | ... | @@ -13,11 +13,11 @@ |
| 13 | 13 |
import type { MerchantDetailQuery } from '@/lib/domain/merchant-detail-query';
|
| 14 | 14 |
|
| 15 | 15 |
/** |
| 16 |
- * 가맹몰 상세 Repository — edupay-backend origin/feature/voc b009025(2026-09-17) 기준. |
|
| 16 |
+ * 가맹몰 상세 Repository — edupay-backend origin/develop 8b1bf81(feature/voc 병합, 2026-09-17) 기준. |
|
| 17 | 17 |
* |
| 18 | 18 |
* ``` |
| 19 | 19 |
* GET /api/v1/mngr/voc/detail/pagination 상세가 있는 가맹점만(TB_COM_VOUCHER ⋈ TB_COM_VOUCHER_DTL ⋈ 코드) |
| 20 |
- * GET /api/v1/mngr/voc/detail/{frcsNo} 상세가 없으면 NOT_FOUND 봉투
|
|
| 20 |
+ * GET /api/v1/mngr/voc/detail/{frcsNo} 상세가 없으면 성공 봉투에 data: null (구 버전은 NOT_FOUND)
|
|
| 21 | 21 |
* POST /api/v1/mngr/voc/detail @RequestBody · 상세가 있으면 409, 가맹점이 없으면 404 |
| 22 | 22 |
* PUT /api/v1/mngr/voc/detail/{frcsNo} @RequestBody · 전 컬럼 덮어씀
|
| 23 | 23 |
* DELETE /api/v1/mngr/voc/{frcsNo} 논리 삭제(DEL_YN)
|
... | ... | @@ -29,11 +29,13 @@ |
| 29 | 29 |
* (`frcsTpbizCdNm`·`frcsBzstatCdNm`·`frcsCdNm`)도 오지만 화면(시안)에 자리가 없어 읽지 않는다. |
| 30 | 30 |
* |
| 31 | 31 |
* ⚠️ 백엔드 결함(보고함, 미수정): |
| 32 |
- * 1. 응답 VO(`MngrVocDetailVo`)에 `frcsNm`이 없어 SELECT의 업체명이 버려진다 — 그동안 업체명 자리에 |
|
| 33 |
- * 가맹점 코드를 보인다. |
|
| 34 |
- * 2. 건수 쿼리(`selectPaginationCount`)만 LEFT JOIN이라 상세 없는 가맹점까지 세어 페이지 수가 부풀 수 있다. |
|
| 35 |
- * 3. `selectOne`이 DEL_YN을 거르지 않아 삭제한 상세가 있는 가맹점은 다시 등록하면 409가 난다. |
|
| 36 |
- * 4. `searchStartDt/EndDt`를 VO만 받고 WHERE에 쓰지 않는다. |
|
| 32 |
+ * 1. 응답 VO(`MngrVocDetailVo`)에 `frcsNm`이 없어 SELECT의 업체명이 버려진다 — 목록은 가맹점 목록의 |
|
| 33 |
+ * 이름으로 메우고, 단건은 코드만 오면 가맹점 목록을 한 번 더 읽는다. |
|
| 34 |
+ * 2. `selectOne`이 DEL_YN을 거르지 않는다. 삭제한 상세가 남은 가맹점은 목록·앱에서는 빠지는데 단건은 |
|
| 35 |
+ * 삭제된 값을 돌려주고(수정 화면이 그 값을 보인다) 등록은 409로 막힌다 — 삭제 뒤 재등록 불가. |
|
| 36 |
+ * 응답에 delYn이 없어(@JsonIgnore) 프론트가 가려낼 수도 없다. |
|
| 37 |
+ * 3. 건수 쿼리(`selectPaginationCount`)만 LEFT JOIN이라 부풀어 온다 — 목록을 여기서 합쳐 세므로 쓰지 않는다. |
|
| 38 |
+ * 4. `searchStartDt/EndDt`를 VO만 받고 WHERE에 쓰지 않는다 — 여기서 등록일로 거른다. |
|
| 37 | 39 |
* 5. 「바우처 사용」을 담을 필드가 없다 — `voucherUseYn`으로 보내되 버려진다. |
| 38 | 40 |
*/ |
| 39 | 41 |
|
... | ... | @@ -283,7 +285,7 @@ |
| 283 | 285 |
); |
| 284 | 286 |
|
| 285 | 287 |
if (!result.ok) {
|
| 286 |
- // 상세가 없는 가맹점은 NOT_FOUND 봉투로 온다 — 화면이 notFound()로 넘기게 null을 준다. |
|
| 288 |
+ // 구 버전은 상세가 없으면 NOT_FOUND 봉투였다. 지금은 성공 봉투에 data: null이라 아래 toDetail이 null을 준다. |
|
| 287 | 289 |
if (result.code === 404) return null; |
| 288 | 290 |
throw new BackendRequestError(result); |
| 289 | 291 |
} |
--- lib/data/repositories/merchant-product-repository.ts
+++ lib/data/repositories/merchant-product-repository.ts
... | ... | @@ -114,7 +114,7 @@ |
| 114 | 114 |
); |
| 115 | 115 |
|
| 116 | 116 |
if (!result.ok) {
|
| 117 |
- // 없는 메뉴는 404 봉투로 온다 — 화면이 notFound()로 넘기게 null을 준다. |
|
| 117 |
+ // 구 버전은 없는 메뉴가 404 봉투였다. 지금은 성공 봉투에 data: null이라 아래에서 null이 된다. |
|
| 118 | 118 |
if (result.code === 404) return null; |
| 119 | 119 |
throw new BackendRequestError(result); |
| 120 | 120 |
} |
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?