diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistController.kt b/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistController.kt index 1c3a348a4..5d0ddd844 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistController.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistController.kt @@ -3,7 +3,6 @@ package com.depromeet.piki.wishlist.controller import com.depromeet.piki.common.response.ApiResponseBody import com.depromeet.piki.common.response.PageResponse import com.depromeet.piki.image.controller.dto.ConfirmImageUploadRequest -import com.depromeet.piki.product.source.SourcePlatformResolver import com.depromeet.piki.image.controller.dto.PresignedImageUploadRequest import com.depromeet.piki.image.controller.dto.PresignedImageUploadResponse import com.depromeet.piki.wishlist.controller.dto.WishDetailResponse @@ -36,10 +35,8 @@ import java.util.UUID @RequestMapping("/api/v1/wishlists") class WishlistController( private val wishlistService: WishlistService, - private val sourcePlatformResolver: SourcePlatformResolver, ) : WishlistApi { - private fun toResponse(result: WishWithItem): WishItemResponse = - WishItemResponse.from(result.wish, result.item, result.snapshot, sourcePlatformResolver.resolve(result.item.link)) + private fun toResponse(result: WishWithItem): WishItemResponse = WishItemResponse.from(result) @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -49,7 +46,7 @@ class WishlistController( ): ApiResponseBody { val result = wishlistService.registerFromUrl(rawUrl = request.url, userId = userId) return ApiResponseBody.created( - WishItemResponse.fromRegistration(result, sourcePlatformResolver.resolve(result.item.link)), + WishItemResponse.fromRegistration(result), ) } @@ -93,7 +90,7 @@ class WishlistController( ): ApiResponseBody { val result = wishlistService.getWish(userId = userId, wishId = wishId) return ApiResponseBody.ok( - WishDetailResponse.from(result, sourcePlatformResolver.resolve(result.item.link), requesterId = userId), + WishDetailResponse.from(result, requesterId = userId), ) } diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishDetailResponse.kt b/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishDetailResponse.kt index 77e5ca4a8..473b06850 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishDetailResponse.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishDetailResponse.kt @@ -35,17 +35,15 @@ data class WishDetailResponse( val priceHistory: List, ) { companion object { - // sourcePlatform 은 SourcePlatformResolver(빈)의 판정이라 호출부(컨트롤러)가 풀어 넘긴다. // requesterId 는 editedByMe(수기 버전을 내가 넣었는지) 파생용 — 편집자 식별자(UUID)는 응답에 노출하지 않는다. fun from( detail: WishDetail, - sourcePlatform: String?, requesterId: UUID, ): WishDetailResponse = WishDetailResponse( wish = WishItemResponse.WishView.from(detail.wish), memo = detail.wish.memo, - item = WishItemResponse.ItemView.from(detail.item, detail.snapshot, sourcePlatform), + item = WishItemResponse.ItemView.from(detail.item, detail.snapshot, detail.sourcePlatform), priceHistory = detail.history.map { PriceHistoryEntry.from(it, requesterId) }, ) } diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishItemResponse.kt b/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishItemResponse.kt index 91b66271c..946105510 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishItemResponse.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/controller/dto/WishItemResponse.kt @@ -33,25 +33,19 @@ data class WishItemResponse( ) { companion object { fun from( - wish: Wish, - item: Item, - snapshot: ItemSnapshot, - sourcePlatform: String?, + result: WishWithItem, ): WishItemResponse = WishItemResponse( - wish = WishView.from(wish), - item = ItemView.from(item, snapshot, sourcePlatform), + wish = WishView.from(result.wish), + item = ItemView.from(result), ) // URL 등록 응답 전용 — 공유 attach 메타(#853)까지 싣는다. 등록만 이 오버로드를 쓰고, // 목록·수정 등 다른 경로는 위 기본 from(플래그 null)을 유지한다. - fun fromRegistration( - result: WishWithItem, - sourcePlatform: String?, - ): WishItemResponse = + fun fromRegistration(result: WishWithItem): WishItemResponse = WishItemResponse( wish = WishView.from(result.wish), - item = ItemView.from(result.item, result.snapshot, sourcePlatform), + item = ItemView.from(result), reused = result.reused, refreshNeeded = result.refreshNeeded, ) @@ -123,7 +117,8 @@ data class WishItemResponse( companion object { // 표시값(status·name·price·currency·imageUrl)은 활성 snapshot 에서, // 정체성(id·sourceUrl=상품 링크)은 item 에서 읽는다. snapshot 은 5단계 갱신에서 새 버전으로 스왑된다. - // sourcePlatform 은 SourcePlatformResolver(빈)의 판정이라 호출부(컨트롤러)가 풀어 넘긴다. + fun from(result: WishWithItem): ItemView = from(result.item, result.snapshot, result.sourcePlatform) + fun from( item: Item, snapshot: ItemSnapshot, diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/service/WishlistService.kt b/src/main/kotlin/com/depromeet/piki/wishlist/service/WishlistService.kt index f6502b6d7..f9e7881d8 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/service/WishlistService.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/service/WishlistService.kt @@ -15,6 +15,7 @@ import com.depromeet.piki.item.repository.ItemSnapshotRepository import com.depromeet.piki.item.service.ItemDisplayService import com.depromeet.piki.item.service.ItemRegistrar import com.depromeet.piki.product.domain.ProductLink +import com.depromeet.piki.product.source.SourcePlatformResolver import com.depromeet.piki.user.domain.IdentityType import com.depromeet.piki.user.service.UserService import com.depromeet.piki.wishlist.domain.WishCursor @@ -43,8 +44,11 @@ class WishlistService( private val itemDisplayService: ItemDisplayService, private val itemQuotaGuard: ItemQuotaGuard, private val userService: UserService, + private val sourcePlatformResolver: SourcePlatformResolver, ) { + private fun WishWithItem.withSourcePlatform(): WishWithItem = copy(sourcePlatform = sourcePlatformResolver.resolve(item.link)) + // TODO AOP private fun requireMember(userId: UUID) { val user = userService.findActiveById(userId) @@ -59,7 +63,7 @@ class WishlistService( val link = ProductLink.parse(rawUrl) wishPersistenceService.rejectIfAlreadyRegistered(userId, link) itemRegistrar.accept(link, userId) - return wishPersistenceService.persist(userId, link) + return wishPersistenceService.persist(userId, link).withSourcePlatform() } fun presignImageUploads( @@ -79,9 +83,10 @@ class WishlistService( requireMember(userId) if (imageKeys.size !in MIN_IMAGE_COUNT..MAX_IMAGE_COUNT) throw WishException.invalidImageCount() imagePresignService.verifyUploaded(imageKeys) - return imageKeys.mapNotNull { key -> - skippingDuplicateKey(Item.SOURCE_IMAGE_KEY_UNIQUE) { wishPersistenceService.registerImage(key, userId) } - } + return imageKeys + .mapNotNull { key -> + skippingDuplicateKey(Item.SOURCE_IMAGE_KEY_UNIQUE) { wishPersistenceService.registerImage(key, userId) } + }.map { it.withSourcePlatform() } } @Transactional(readOnly = true) @@ -106,7 +111,11 @@ class WishlistService( pageWishes.map { wish -> // item 은 wish 와 함께 영속화되며 병합 시 함께 옮겨진다(WishItemMergeListener). 없으면 영속화 경로가 깨진 코드 버그다. val item = itemsById[wish.itemId] ?: error("wish ${wish.getId()} 의 item ${wish.itemId} 가 없다") - WishWithItem(wish = wish, item = item, snapshot = displayByCard.getValue(wish.displayCard())) + WishWithItem( + wish = wish, + item = item, + snapshot = displayByCard.getValue(wish.displayCard()), + ).withSourcePlatform() } val nextCursor = @@ -141,6 +150,7 @@ class WishlistService( item = item, snapshot = itemDisplayService.resolveDisplay(wish.displayCard()), history = history, + sourcePlatform = sourcePlatformResolver.resolve(item.link), ) } @@ -163,7 +173,7 @@ class WishlistService( if (listOfNotNull(name, price, currency, image).isEmpty()) { memo?.let { // 응답의 item 은 updateMemo 가 행 락 안에서 표시값(#857)으로 맞춰 돌려준다. - return wishPersistenceService.updateMemo(userId = userId, wishId = wishId, memo = it) + return wishPersistenceService.updateMemo(userId = userId, wishId = wishId, memo = it).withSourcePlatform() } } // 이미지 형식 검증(빈 바이트·미지원 MIME) — 외부 호출 전에 동기로 거른다(400). @@ -186,7 +196,7 @@ class WishlistService( imageUrl = imageUrl, currency = currency, memo = memo, - ) + ).withSourcePlatform() } // 위시 item 의 상품 정보를 원본 링크로 재추출해 최신화한다(수동 새로고침). 추출(Gemini)은 디스패처가 비동기로 @@ -201,7 +211,7 @@ class WishlistService( // refresh 계약 검증(링크 없음·FAILED 항목 등)은 persistence 안쪽이라 여기선 앞서 깎이는데, 그 두 사유는 // 클라가 refresh 버튼을 띄우지 않는 상태라 정상 흐름에서 반복 호출되지 않는다. itemQuotaGuard.consume(userId, 1, ItemErrorCode.QUOTA_EXCEEDED) - return wishPersistenceService.refresh(userId = userId, wishId = wishId) + return wishPersistenceService.refresh(userId = userId, wishId = wishId).withSourcePlatform() } // 멱등 삭제: 없거나 이미 삭제됐으면 "이미 목표 상태(없음)"이므로 성공으로 본다(no-op). diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishDetail.kt b/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishDetail.kt index 70eb1c678..f63d7c84e 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishDetail.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishDetail.kt @@ -16,4 +16,5 @@ data class WishDetail( val item: Item, val snapshot: ItemSnapshot, val history: List, + val sourcePlatform: String?, ) diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishWithItem.kt b/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishWithItem.kt index e2420c600..e42fbb0b4 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishWithItem.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/service/dto/WishWithItem.kt @@ -7,10 +7,12 @@ import com.depromeet.piki.wishlist.domain.Wish // wish 기록과 그 wish 가 가리키는 상품의 정체성(item)·활성 버전(snapshot) 묶음. 등록 결과·조회 항목이 공유한다. // 표시값(name/price/image/status)은 snapshot 에서, 정체성(id·sourceUrl=link)은 item 에서 온다. // reused·refreshNeeded 는 공유 등록(#853)의 attach 메타 — 등록 경로만 채우고 그 외(조회·수기 수정 등)는 기본값 false. +// sourcePlatform 은 응답 경계(WishlistService)가 링크에서 판정해 채운다. 영속화 계층은 비워 둔다. data class WishWithItem( val wish: Wish, val item: Item, val snapshot: ItemSnapshot, val reused: Boolean = false, val refreshNeeded: Boolean = false, + val sourcePlatform: String? = null, ) diff --git a/src/test/kotlin/com/depromeet/piki/wishlist/controller/WishDisplayIntegrationTest.kt b/src/test/kotlin/com/depromeet/piki/wishlist/controller/WishDisplayIntegrationTest.kt index 1ec548944..f6d1f267e 100644 --- a/src/test/kotlin/com/depromeet/piki/wishlist/controller/WishDisplayIntegrationTest.kt +++ b/src/test/kotlin/com/depromeet/piki/wishlist/controller/WishDisplayIntegrationTest.kt @@ -62,6 +62,7 @@ class WishDisplayIntegrationTest : IntegrationTestSupport() { .andExpect(status().isOk) .andExpect(jsonPath("$.data[0].item.name").value("새 기계값")) .andExpect(jsonPath("$.data[0].item.price").value(90_000)) + .andExpect(jsonPath("$.data[0].item.sourcePlatform").value("example")) mockMvc .perform(get("/api/v1/wishlists/$wishId").header(HttpHeaders.AUTHORIZATION, "Bearer ${memberToken(userA)}")) .andExpect(status().isOk)