From d0b3c65909993ef987703da4eae833534c0996f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:10:30 +0000 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20Admin=20=EC=88=A0=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=B0=B8=EC=A1=B0?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 술(Alcohol) 단건 상세 조회 API 추가 (GET /alcohols/{id}) - 테이스팅 태그 목록 조회 API 추가 (GET /tasting-tags) - 지역 목록 조회 API 추가 (GET /regions) - 증류소 목록 조회 API 추가 (GET /distilleries) - 관련 DTO, Repository, Service 메서드 추가 - 통합 테스트 추가 (해피케이스 + 인증 방어로직) --- .../persentaton/AdminAlcoholsController.kt | 6 + .../persentaton/AdminDistilleryController.kt | 21 +++ .../persentaton/AdminRegionController.kt | 21 +++ .../persentaton/AdminTastingTagController.kt | 21 +++ .../alcohols/AdminAlcoholsIntegrationTest.kt | 54 +++++++ .../AdminReferenceDataIntegrationTest.kt | 150 ++++++++++++++++++ .../domain/AlcoholQueryRepository.java | 4 + .../alcohols/domain/DistilleryRepository.java | 9 ++ .../alcohols/domain/RegionRepository.java | 3 + .../alcohols/domain/TastingTagRepository.java | 3 + .../response/AdminAlcoholDetailResponse.java | 35 ++++ .../dto/response/AdminDistilleryItem.java | 11 ++ .../dto/response/AdminRegionItem.java | 12 ++ .../dto/response/AdminTastingTagItem.java | 12 ++ .../CustomAlcoholQueryRepository.java | 30 ++++ .../CustomAlcoholQueryRepositoryImpl.java | 80 ++++++++++ .../repository/JpaDistilleryRepository.java | 24 +++ .../repository/JpaRegionQueryRepository.java | 17 +- .../repository/JpaTastingTagRepository.java | 16 +- .../alcohols/service/AlcoholQueryService.java | 57 +++++++ 20 files changed, 582 insertions(+), 4 deletions(-) create mode 100644 bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt create mode 100644 bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt create mode 100644 bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt create mode 100644 bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminAlcoholDetailResponse.java create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminDistilleryItem.java create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminRegionItem.java create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminTastingTagItem.java create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt index c9f98a6ed..db8a37305 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt @@ -6,6 +6,7 @@ import app.bottlenote.global.data.response.GlobalResponse import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -20,4 +21,9 @@ class AdminAlcoholsController( fun searchAlcohols(@ModelAttribute request: AdminAlcoholSearchRequest): ResponseEntity { return ResponseEntity.ok(alcoholQueryService.searchAdminAlcohols(request)) } + + @GetMapping("/{alcoholId}") + fun getAlcoholDetail(@PathVariable alcoholId: Long): ResponseEntity<*> { + return ResponseEntity.ok(GlobalResponse.ok(alcoholQueryService.findAdminAlcoholDetailById(alcoholId))) + } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt new file mode 100644 index 000000000..eda988c71 --- /dev/null +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt @@ -0,0 +1,21 @@ +package app.bottlenote.alcohols.persentaton + +import app.bottlenote.alcohols.domain.DistilleryRepository +import app.bottlenote.global.data.response.GlobalResponse +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/distilleries") +class AdminDistilleryController( + private val distilleryRepository: DistilleryRepository +) { + + @GetMapping + fun getAllDistilleries(): ResponseEntity<*> { + return ResponseEntity.ok(GlobalResponse.ok(distilleryRepository.findAllDistilleries())) + } +} diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt new file mode 100644 index 000000000..ff26d6217 --- /dev/null +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt @@ -0,0 +1,21 @@ +package app.bottlenote.alcohols.persentaton + +import app.bottlenote.alcohols.domain.RegionRepository +import app.bottlenote.global.data.response.GlobalResponse +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/regions") +class AdminRegionController( + private val regionRepository: RegionRepository +) { + + @GetMapping + fun getAllRegions(): ResponseEntity<*> { + return ResponseEntity.ok(GlobalResponse.ok(regionRepository.findAllRegions())) + } +} diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt new file mode 100644 index 000000000..6faa0afb4 --- /dev/null +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt @@ -0,0 +1,21 @@ +package app.bottlenote.alcohols.persentaton + +import app.bottlenote.alcohols.domain.TastingTagRepository +import app.bottlenote.global.data.response.GlobalResponse +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/tasting-tags") +class AdminTastingTagController( + private val tastingTagRepository: TastingTagRepository +) { + + @GetMapping + fun getAllTastingTags(): ResponseEntity<*> { + return ResponseEntity.ok(GlobalResponse.ok(tastingTagRepository.findAllTastingTags())) + } +} diff --git a/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminAlcoholsIntegrationTest.kt b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminAlcoholsIntegrationTest.kt index 579c62c6b..064488f8b 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminAlcoholsIntegrationTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminAlcoholsIntegrationTest.kt @@ -8,6 +8,7 @@ import app.bottlenote.global.service.cursor.SortOrder import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -148,4 +149,57 @@ class AdminAlcoholsIntegrationTest : IntegrationTestSupport() { .bodyJson() .extractingPath("$.meta.size").isEqualTo(size) } + + @Nested + @DisplayName("술 단건 상세 조회 API") + inner class GetAlcoholDetail { + + @Test + @DisplayName("관리자용 술 단건 상세 정보를 조회할 수 있다") + fun getAlcoholDetailSuccess() { + // given + val alcohol = alcoholTestFactory.persistAlcoholWithName("글렌피딕 12년", "Glenfiddich 12") + + // when & then - 성공 응답 확인 + assertThat( + mockMvcTester.get().uri("/alcohols/${alcohol.id}") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + + // 상세 데이터 검증 + assertThat( + mockMvcTester.get().uri("/alcohols/${alcohol.id}") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.data.korName").isEqualTo("글렌피딕 12년") + } + + @Test + @DisplayName("모든 상세 필드가 포함되어 응답한다") + fun getAlcoholDetailWithAllFields() { + // given + val alcohol = alcoholTestFactory.persistAlcoholWithName("맥캘란 18년", "Macallan 18") + + // when & then - 필수 필드 존재 여부 확인 + val result = mockMvcTester.get().uri("/alcohols/${alcohol.id}") + .header("Authorization", "Bearer $accessToken") + + assertThat(result) + .hasStatusOk() + .bodyJson() + .extractingPath("$.data.alcoholId").isNotNull + + // 방어로직: 존재하지 않는 ID로 조회 시 실패 + assertThat( + mockMvcTester.get().uri("/alcohols/999999") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatus4xxClientError() + } + } } diff --git a/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt new file mode 100644 index 000000000..e5dafed05 --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt @@ -0,0 +1,150 @@ +package app.integration.alcohols + +import app.IntegrationTestSupport +import app.bottlenote.alcohols.fixture.AlcoholTestFactory +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +@Tag("admin_integration") +@DisplayName("[integration] Admin 참조 데이터 API 통합 테스트") +class AdminReferenceDataIntegrationTest : IntegrationTestSupport() { + + @Autowired + private lateinit var alcoholTestFactory: AlcoholTestFactory + + private lateinit var accessToken: String + + @BeforeEach + fun setUp() { + val admin = adminUserTestFactory.persistRootAdmin() + accessToken = getAccessToken(admin) + } + + @Nested + @DisplayName("테이스팅 태그 목록 조회 API") + inner class TastingTagsApi { + + @Test + @DisplayName("전체 테이스팅 태그 목록을 조회할 수 있다") + fun getAllTastingTagsSuccess() { + // when & then + assertThat( + mockMvcTester.get().uri("/tasting-tags") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + + // 응답이 리스트 형태임을 확인 + assertThat( + mockMvcTester.get().uri("/tasting-tags") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.data").isNotNull + } + + @Test + @DisplayName("인증 없이 요청 시 실패한다") + fun getTastingTagsWithoutAuth() { + // when & then - 방어로직: 인증 없이 요청 시 실패 + assertThat( + mockMvcTester.get().uri("/tasting-tags") + ) + .hasStatus4xxClientError() + } + } + + @Nested + @DisplayName("지역 목록 조회 API") + inner class RegionsApi { + + @Test + @DisplayName("전체 지역 목록을 조회할 수 있다") + fun getAllRegionsSuccess() { + // given - alcoholTestFactory에서 region 데이터가 함께 생성됨 + alcoholTestFactory.persistAlcohols(1) + + // when & then + assertThat( + mockMvcTester.get().uri("/regions") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + } + + @Test + @DisplayName("지역 목록이 메타 정보를 포함한다") + fun getRegionsWithMetaInfo() { + // given + alcoholTestFactory.persistAlcohols(1) + + // when & then - 응답 데이터 확인 + val result = mockMvcTester.get().uri("/regions") + .header("Authorization", "Bearer $accessToken") + + assertThat(result) + .hasStatusOk() + .bodyJson() + .extractingPath("$.data").isNotNull + + // 방어로직: 인증 없이 요청 시 실패 + assertThat( + mockMvcTester.get().uri("/regions") + ) + .hasStatus4xxClientError() + } + } + + @Nested + @DisplayName("증류소 목록 조회 API") + inner class DistilleriesApi { + + @Test + @DisplayName("전체 증류소 목록을 조회할 수 있다") + fun getAllDistilleriesSuccess() { + // given - alcoholTestFactory에서 distillery 데이터가 함께 생성됨 + alcoholTestFactory.persistAlcohols(1) + + // when & then + assertThat( + mockMvcTester.get().uri("/distilleries") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + } + + @Test + @DisplayName("증류소 목록이 필수 필드를 포함한다") + fun getDistilleriesWithRequiredFields() { + // given + alcoholTestFactory.persistAlcohols(1) + + // when & then + val result = mockMvcTester.get().uri("/distilleries") + .header("Authorization", "Bearer $accessToken") + + assertThat(result) + .hasStatusOk() + .bodyJson() + .extractingPath("$.data").isNotNull + + // 방어로직: 인증 없이 요청 시 실패 + assertThat( + mockMvcTester.get().uri("/distilleries") + ) + .hasStatus4xxClientError() + } + } +} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java index 99f5a9444..6f70cdd7e 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java @@ -15,6 +15,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.springframework.data.domain.Page; +import static app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; + /** 알코올 조회 질의에 관한 애그리거트를 정의합니다. */ public interface AlcoholQueryRepository { @@ -40,4 +42,6 @@ Pair> getStandardExplore( Long userId, List keyword, Long cursor, Integer size); Page searchAdminAlcohols(AdminAlcoholSearchRequest request); + + Optional findAdminAlcoholDetailById(Long alcoholId); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java new file mode 100644 index 000000000..902f4c8ff --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java @@ -0,0 +1,9 @@ +package app.bottlenote.alcohols.domain; + +import app.bottlenote.alcohols.dto.response.AdminDistilleryItem; +import java.util.List; + +public interface DistilleryRepository { + + List findAllDistilleries(); +} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java index e406586e9..796250fdf 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java @@ -1,9 +1,12 @@ package app.bottlenote.alcohols.domain; +import app.bottlenote.alcohols.dto.response.AdminRegionItem; import app.bottlenote.alcohols.dto.response.RegionsItem; import java.util.List; public interface RegionRepository { List findAllRegionsResponse(); + + List findAllRegions(); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java index fd20bf58e..a28625d5f 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java @@ -1,8 +1,11 @@ package app.bottlenote.alcohols.domain; +import app.bottlenote.alcohols.dto.response.AdminTastingTagItem; import java.util.List; public interface TastingTagRepository { List findAll(); + + List findAllTastingTags(); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminAlcoholDetailResponse.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminAlcoholDetailResponse.java new file mode 100644 index 000000000..87996f4c4 --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminAlcoholDetailResponse.java @@ -0,0 +1,35 @@ +package app.bottlenote.alcohols.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +public record AdminAlcoholDetailResponse( + Long alcoholId, + String korName, + String engName, + String imageUrl, + String type, + String korCategory, + String engCategory, + String categoryGroup, + String abv, + String age, + String cask, + String volume, + String description, + Long regionId, + String korRegion, + String engRegion, + Long distilleryId, + String korDistillery, + String engDistillery, + List tastingTags, + Double avgRating, + Long totalRatingsCount, + Long reviewCount, + Long pickCount, + LocalDateTime createdAt, + LocalDateTime modifiedAt) { + + public record TastingTagInfo(Long id, String korName, String engName) {} +} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminDistilleryItem.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminDistilleryItem.java new file mode 100644 index 000000000..8e1c5503c --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminDistilleryItem.java @@ -0,0 +1,11 @@ +package app.bottlenote.alcohols.dto.response; + +import java.time.LocalDateTime; + +public record AdminDistilleryItem( + Long id, + String korName, + String engName, + String logoImgUrl, + LocalDateTime createdAt, + LocalDateTime modifiedAt) {} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminRegionItem.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminRegionItem.java new file mode 100644 index 000000000..34655c496 --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminRegionItem.java @@ -0,0 +1,12 @@ +package app.bottlenote.alcohols.dto.response; + +import java.time.LocalDateTime; + +public record AdminRegionItem( + Long id, + String korName, + String engName, + String continent, + String description, + LocalDateTime createdAt, + LocalDateTime modifiedAt) {} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminTastingTagItem.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminTastingTagItem.java new file mode 100644 index 000000000..c7fa78074 --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/response/AdminTastingTagItem.java @@ -0,0 +1,12 @@ +package app.bottlenote.alcohols.dto.response; + +import java.time.LocalDateTime; + +public record AdminTastingTagItem( + Long id, + String korName, + String engName, + String icon, + String description, + LocalDateTime createdAt, + LocalDateTime modifiedAt) {} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java index e50794619..4fd4ce410 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Optional; import org.apache.commons.lang3.tuple.Pair; +import java.time.LocalDateTime; import org.springframework.data.domain.Page; public interface CustomAlcoholQueryRepository { @@ -25,4 +26,33 @@ Pair> getStandardExplore( Long userId, List keyword, Long cursor, Integer size); Page searchAdminAlcohols(AdminAlcoholSearchRequest request); + + Optional findAdminAlcoholDetailById(Long alcoholId); + + record AdminAlcoholDetailProjection( + Long alcoholId, + String korName, + String engName, + String imageUrl, + String type, + String korCategory, + String engCategory, + String categoryGroup, + String abv, + String age, + String cask, + String volume, + String description, + Long regionId, + String korRegion, + String engRegion, + Long distilleryId, + String korDistillery, + String engDistillery, + Double avgRating, + Long totalRatingsCount, + Long reviewCount, + Long pickCount, + LocalDateTime createdAt, + LocalDateTime modifiedAt) {} } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepositoryImpl.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepositoryImpl.java index 1308d6d46..c390c610f 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepositoryImpl.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepositoryImpl.java @@ -330,4 +330,84 @@ public Page searchAdminAlcohols(AdminAlcoholSearchRequest requ return new PageImpl<>( content, PageRequest.of(request.page(), request.size()), total != null ? total : 0L); } + + /** Admin용 알코올 단건 상세 조회 */ + @Override + public Optional findAdminAlcoholDetailById(Long alcoholId) { + AdminAlcoholDetailProjection result = + queryFactory + .select( + Projections.constructor( + AdminAlcoholDetailProjection.class, + alcohol.id, + alcohol.korName, + alcohol.engName, + alcohol.imageUrl, + alcohol.type.stringValue(), + alcohol.korCategory, + alcohol.engCategory, + alcohol.categoryGroup.stringValue(), + alcohol.abv, + alcohol.age, + alcohol.cask, + alcohol.volume, + alcohol.description, + region.id, + region.korName, + region.engName, + distillery.id, + distillery.korName, + distillery.engName, + rating + .ratingPoint + .rating + .avg() + .multiply(2) + .castToNum(Double.class) + .round() + .divide(2) + .coalesce(0.0), + rating.id.count(), + review.id.countDistinct(), + picks.id.countDistinct(), + alcohol.createAt, + alcohol.lastModifyAt)) + .from(alcohol) + .leftJoin(rating) + .on(rating.id.alcoholId.eq(alcohol.id)) + .leftJoin(review) + .on(review.alcoholId.eq(alcohol.id)) + .leftJoin(picks) + .on(picks.alcoholId.eq(alcohol.id)) + .leftJoin(region) + .on(alcohol.region.id.eq(region.id)) + .leftJoin(distillery) + .on(alcohol.distillery.id.eq(distillery.id)) + .where(alcohol.id.eq(alcoholId)) + .groupBy( + alcohol.id, + alcohol.korName, + alcohol.engName, + alcohol.imageUrl, + alcohol.type, + alcohol.korCategory, + alcohol.engCategory, + alcohol.categoryGroup, + alcohol.abv, + alcohol.age, + alcohol.cask, + alcohol.volume, + alcohol.description, + region.id, + region.korName, + region.engName, + distillery.id, + distillery.korName, + distillery.engName, + alcohol.createAt, + alcohol.lastModifyAt) + .fetchOne(); + + return Optional.ofNullable(result); + } } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java new file mode 100644 index 000000000..9b8fe4686 --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java @@ -0,0 +1,24 @@ +package app.bottlenote.alcohols.repository; + +import app.bottlenote.alcohols.domain.Distillery; +import app.bottlenote.alcohols.domain.DistilleryRepository; +import app.bottlenote.alcohols.dto.response.AdminDistilleryItem; +import app.bottlenote.common.annotation.JpaRepositoryImpl; +import java.util.List; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +@JpaRepositoryImpl +public interface JpaDistilleryRepository + extends DistilleryRepository, CrudRepository { + + @Override + @Query( + """ + select new app.bottlenote.alcohols.dto.response.AdminDistilleryItem( + d.id, d.korName, d.engName, d.logoImgPath, d.createAt, d.lastModifyAt + ) + from distillery d order by d.id asc + """) + List findAllDistilleries(); +} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java index 605d57a38..449652815 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java @@ -2,6 +2,7 @@ import app.bottlenote.alcohols.domain.Region; import app.bottlenote.alcohols.domain.RegionRepository; +import app.bottlenote.alcohols.dto.response.AdminRegionItem; import app.bottlenote.alcohols.dto.response.RegionsItem; import app.bottlenote.common.annotation.JpaRepositoryImpl; import java.util.List; @@ -14,8 +15,18 @@ public interface JpaRegionQueryRepository extends RegionRepository, CrudReposito @Override @Query( """ - select new app.bottlenote.alcohols.dto.response.RegionsItem(r.id, r.korName, r.engName, r.description) - from region r order by r.id asc - """) + select new app.bottlenote.alcohols.dto.response.RegionsItem(r.id, r.korName, r.engName, r.description) + from region r order by r.id asc + """) List findAllRegionsResponse(); + + @Override + @Query( + """ + select new app.bottlenote.alcohols.dto.response.AdminRegionItem( + r.id, r.korName, r.engName, r.continent, r.description, r.createAt, r.lastModifyAt + ) + from region r order by r.id asc + """) + List findAllRegions(); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java index fbbf74463..62f4032ad 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java @@ -2,9 +2,23 @@ import app.bottlenote.alcohols.domain.TastingTag; import app.bottlenote.alcohols.domain.TastingTagRepository; +import app.bottlenote.alcohols.dto.response.AdminTastingTagItem; import app.bottlenote.common.annotation.JpaRepositoryImpl; +import java.util.List; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @JpaRepositoryImpl public interface JpaTastingTagRepository - extends TastingTagRepository, CrudRepository {} + extends TastingTagRepository, CrudRepository { + + @Override + @Query( + """ + select new app.bottlenote.alcohols.dto.response.AdminTastingTagItem( + t.id, t.korName, t.engName, t.icon, t.description, t.createAt, t.lastModifyAt + ) + from tasting_tag t order by t.id asc + """) + List findAllTastingTags(); +} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholQueryService.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholQueryService.java index 58fe781fb..4f6394a97 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholQueryService.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholQueryService.java @@ -1,13 +1,19 @@ package app.bottlenote.alcohols.service; +import app.bottlenote.alcohols.domain.Alcohol; import app.bottlenote.alcohols.domain.AlcoholQueryRepository; import app.bottlenote.alcohols.dto.dsl.AlcoholSearchCriteria; import app.bottlenote.alcohols.dto.request.AdminAlcoholSearchRequest; import app.bottlenote.alcohols.dto.request.AlcoholSearchRequest; +import app.bottlenote.alcohols.dto.response.AdminAlcoholDetailResponse; +import app.bottlenote.alcohols.dto.response.AdminAlcoholDetailResponse.TastingTagInfo; import app.bottlenote.alcohols.dto.response.AlcoholDetailItem; import app.bottlenote.alcohols.dto.response.AlcoholDetailResponse; import app.bottlenote.alcohols.dto.response.AlcoholSearchResponse; import app.bottlenote.alcohols.dto.response.FriendsDetailResponse; +import app.bottlenote.alcohols.exception.AlcoholException; +import app.bottlenote.alcohols.exception.AlcoholExceptionCode; +import app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.service.cursor.CursorResponse; import app.bottlenote.global.service.cursor.PageResponse; @@ -96,4 +102,55 @@ public Pair> getStandardExplore( public GlobalResponse searchAdminAlcohols(AdminAlcoholSearchRequest request) { return GlobalResponse.fromPage(alcoholQueryRepository.searchAdminAlcohols(request)); } + + @Transactional(readOnly = true) + public AdminAlcoholDetailResponse findAdminAlcoholDetailById(Long alcoholId) { + AdminAlcoholDetailProjection projection = + alcoholQueryRepository + .findAdminAlcoholDetailById(alcoholId) + .orElseThrow(() -> new AlcoholException(AlcoholExceptionCode.ALCOHOL_NOT_FOUND)); + + Alcohol alcohol = + alcoholQueryRepository + .findById(alcoholId) + .orElseThrow(() -> new AlcoholException(AlcoholExceptionCode.ALCOHOL_NOT_FOUND)); + + List tastingTags = + alcohol.getAlcoholsTastingTags().stream() + .map( + att -> + new TastingTagInfo( + att.getTastingTag().getId(), + att.getTastingTag().getKorName(), + att.getTastingTag().getEngName())) + .toList(); + + return new AdminAlcoholDetailResponse( + projection.alcoholId(), + projection.korName(), + projection.engName(), + projection.imageUrl(), + projection.type(), + projection.korCategory(), + projection.engCategory(), + projection.categoryGroup(), + projection.abv(), + projection.age(), + projection.cask(), + projection.volume(), + projection.description(), + projection.regionId(), + projection.korRegion(), + projection.engRegion(), + projection.distilleryId(), + projection.korDistillery(), + projection.engDistillery(), + tastingTags, + projection.avgRating(), + projection.totalRatingsCount(), + projection.reviewCount(), + projection.pickCount(), + projection.createdAt(), + projection.modifiedAt()); + } } From c26b72fab8fac104e2337a503376237d436b5914 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:15:13 +0000 Subject: [PATCH 02/14] =?UTF-8?q?docs:=20Admin=20API=20RestDocs=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 술 단건 상세 조회 RestDocs 테스트 추가 - 테이스팅 태그 목록 RestDocs 테스트 추가 - 지역 목록 RestDocs 테스트 추가 - 증류소 목록 RestDocs 테스트 추가 - AlcoholsHelper에 테스트 데이터 생성 메서드 추가 - asciidoc 문서 확장 (alcohols.adoc, reference.adoc) --- .../src/docs/asciidoc/admin-api.adoc | 6 ++ .../asciidoc/api/admin-alcohols/alcohols.adoc | 27 ++++++ .../api/admin-reference/reference.adoc | 75 +++++++++++++++ .../AdminAlcoholsControllerDocsTest.kt | 69 ++++++++++++++ .../AdminDistilleryControllerDocsTest.kt | 75 +++++++++++++++ .../alcohols/AdminRegionControllerDocsTest.kt | 76 +++++++++++++++ .../AdminTastingTagControllerDocsTest.kt | 76 +++++++++++++++ .../app/helper/alcohols/AlcoholsHelper.kt | 95 +++++++++++++++++++ 8 files changed, 499 insertions(+) create mode 100644 bottlenote-admin-api/src/docs/asciidoc/api/admin-reference/reference.adoc create mode 100644 bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt create mode 100644 bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt create mode 100644 bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt diff --git a/bottlenote-admin-api/src/docs/asciidoc/admin-api.adoc b/bottlenote-admin-api/src/docs/asciidoc/admin-api.adoc index f84daa9a4..8f22e5675 100644 --- a/bottlenote-admin-api/src/docs/asciidoc/admin-api.adoc +++ b/bottlenote-admin-api/src/docs/asciidoc/admin-api.adoc @@ -47,3 +47,9 @@ include::api/admin-help/help.adoc[] == File API include::api/admin-file/file.adoc[] + +''' + +== Reference API + +include::api/admin-reference/reference.adoc[] diff --git a/bottlenote-admin-api/src/docs/asciidoc/api/admin-alcohols/alcohols.adoc b/bottlenote-admin-api/src/docs/asciidoc/api/admin-alcohols/alcohols.adoc index 49f02646a..933bf581c 100644 --- a/bottlenote-admin-api/src/docs/asciidoc/api/admin-alcohols/alcohols.adoc +++ b/bottlenote-admin-api/src/docs/asciidoc/api/admin-alcohols/alcohols.adoc @@ -22,3 +22,30 @@ include::{snippets}/admin/alcohols/search/http-request.adoc[] [discrete] include::{snippets}/admin/alcohols/search/response-fields.adoc[] include::{snippets}/admin/alcohols/search/http-response.adoc[] + +''' + +=== 술(Alcohol) 단건 상세 조회 === + +- 관리자용 술 단건 상세 조회 API입니다. +- 기본 정보, 카테고리, 스펙, 지역, 증류소, 테이스팅 태그, 통계 정보를 모두 포함합니다. + +[source] +---- +GET /admin/api/v1/alcohols/{alcoholId} +---- + +[discrete] +==== 경로 파라미터 ==== + +[discrete] +include::{snippets}/admin/alcohols/detail/path-parameters.adoc[] +include::{snippets}/admin/alcohols/detail/curl-request.adoc[] +include::{snippets}/admin/alcohols/detail/http-request.adoc[] + +[discrete] +==== 응답 파라미터 ==== + +[discrete] +include::{snippets}/admin/alcohols/detail/response-fields.adoc[] +include::{snippets}/admin/alcohols/detail/http-response.adoc[] diff --git a/bottlenote-admin-api/src/docs/asciidoc/api/admin-reference/reference.adoc b/bottlenote-admin-api/src/docs/asciidoc/api/admin-reference/reference.adoc new file mode 100644 index 000000000..6e3f972e3 --- /dev/null +++ b/bottlenote-admin-api/src/docs/asciidoc/api/admin-reference/reference.adoc @@ -0,0 +1,75 @@ +=== 테이스팅 태그 목록 조회 === + +- 전체 테이스팅 태그 목록을 조회합니다. +- 술의 향미를 표현하는 태그 정보를 제공합니다. + +[source] +---- +GET /admin/api/v1/tasting-tags +---- + +[discrete] +==== 요청 ==== + +[discrete] +include::{snippets}/admin/tasting-tags/list/curl-request.adoc[] +include::{snippets}/admin/tasting-tags/list/http-request.adoc[] + +[discrete] +==== 응답 파라미터 ==== + +[discrete] +include::{snippets}/admin/tasting-tags/list/response-fields.adoc[] +include::{snippets}/admin/tasting-tags/list/http-response.adoc[] + +''' + +=== 지역 목록 조회 === + +- 전체 지역(국가) 목록을 조회합니다. +- 술의 원산지 정보를 제공합니다. + +[source] +---- +GET /admin/api/v1/regions +---- + +[discrete] +==== 요청 ==== + +[discrete] +include::{snippets}/admin/regions/list/curl-request.adoc[] +include::{snippets}/admin/regions/list/http-request.adoc[] + +[discrete] +==== 응답 파라미터 ==== + +[discrete] +include::{snippets}/admin/regions/list/response-fields.adoc[] +include::{snippets}/admin/regions/list/http-response.adoc[] + +''' + +=== 증류소 목록 조회 === + +- 전체 증류소 목록을 조회합니다. +- 술을 생산하는 증류소 정보를 제공합니다. + +[source] +---- +GET /admin/api/v1/distilleries +---- + +[discrete] +==== 요청 ==== + +[discrete] +include::{snippets}/admin/distilleries/list/curl-request.adoc[] +include::{snippets}/admin/distilleries/list/http-request.adoc[] + +[discrete] +==== 응답 파라미터 ==== + +[discrete] +include::{snippets}/admin/distilleries/list/response-fields.adoc[] +include::{snippets}/admin/distilleries/list/http-response.adoc[] diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt index 9d49ef6c0..a75ae00bb 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt @@ -3,6 +3,7 @@ package app.docs.alcohols import app.bottlenote.alcohols.constant.AdminAlcoholSortType import app.bottlenote.alcohols.constant.AlcoholCategoryGroup import app.bottlenote.alcohols.dto.request.AdminAlcoholSearchRequest +import app.bottlenote.alcohols.dto.response.AdminAlcoholDetailResponse import app.bottlenote.alcohols.persentaton.AdminAlcoholsController import app.bottlenote.alcohols.service.AlcoholQueryService import app.bottlenote.global.service.cursor.SortOrder @@ -12,6 +13,7 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.BDDMockito.given import org.mockito.Mockito.any +import org.mockito.Mockito.anyLong import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs @@ -22,6 +24,7 @@ import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.responseFields import org.springframework.restdocs.request.RequestDocumentation.parameterWithName +import org.springframework.restdocs.request.RequestDocumentation.pathParameters import org.springframework.restdocs.request.RequestDocumentation.queryParameters import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.assertj.MockMvcTester @@ -117,4 +120,70 @@ class AdminAlcoholsControllerDocsTest { ) ) } + + @Test + @DisplayName("관리자용 술 단건 상세 조회를 할 수 있다") + fun getAlcoholDetail() { + // given + val response = AlcoholsHelper.createAdminAlcoholDetailResponse() + + given(alcoholQueryService.findAdminAlcoholDetailById(anyLong())) + .willReturn(response) + + // when & then + assertThat( + mvc.get().uri("/alcohols/{alcoholId}", 1L) + ) + .hasStatusOk() + .apply( + document( + "admin/alcohols/detail", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("alcoholId").description("술 ID") + ), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("술 상세 정보"), + fieldWithPath("data.alcoholId").type(JsonFieldType.NUMBER).description("술 ID"), + fieldWithPath("data.korName").type(JsonFieldType.STRING).description("한글 이름"), + fieldWithPath("data.engName").type(JsonFieldType.STRING).description("영문 이름"), + fieldWithPath("data.imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("data.type").type(JsonFieldType.STRING).description("술 타입 (WHISKY 등)"), + fieldWithPath("data.korCategory").type(JsonFieldType.STRING).description("카테고리 한글명"), + fieldWithPath("data.engCategory").type(JsonFieldType.STRING).description("카테고리 영문명"), + fieldWithPath("data.categoryGroup").type(JsonFieldType.STRING).description("카테고리 그룹"), + fieldWithPath("data.abv").type(JsonFieldType.STRING).description("도수"), + fieldWithPath("data.age").type(JsonFieldType.STRING).description("숙성년도"), + fieldWithPath("data.cask").type(JsonFieldType.STRING).description("캐스크 타입"), + fieldWithPath("data.volume").type(JsonFieldType.STRING).description("용량"), + fieldWithPath("data.description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("data.regionId").type(JsonFieldType.NUMBER).description("지역 ID"), + fieldWithPath("data.korRegion").type(JsonFieldType.STRING).description("지역 한글명"), + fieldWithPath("data.engRegion").type(JsonFieldType.STRING).description("지역 영문명"), + fieldWithPath("data.distilleryId").type(JsonFieldType.NUMBER).description("증류소 ID"), + fieldWithPath("data.korDistillery").type(JsonFieldType.STRING).description("증류소 한글명"), + fieldWithPath("data.engDistillery").type(JsonFieldType.STRING).description("증류소 영문명"), + fieldWithPath("data.tastingTags").type(JsonFieldType.ARRAY).description("테이스팅 태그 목록"), + fieldWithPath("data.tastingTags[].id").type(JsonFieldType.NUMBER).description("태그 ID"), + fieldWithPath("data.tastingTags[].korName").type(JsonFieldType.STRING).description("태그 한글명"), + fieldWithPath("data.tastingTags[].engName").type(JsonFieldType.STRING).description("태그 영문명"), + fieldWithPath("data.avgRating").type(JsonFieldType.NUMBER).description("평균 평점"), + fieldWithPath("data.totalRatingsCount").type(JsonFieldType.NUMBER).description("평점 수"), + fieldWithPath("data.reviewCount").type(JsonFieldType.NUMBER).description("리뷰 수"), + fieldWithPath("data.pickCount").type(JsonFieldType.NUMBER).description("찜 수"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("생성일시"), + fieldWithPath("data.modifiedAt").type(JsonFieldType.STRING).description("수정일시"), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("API 경로 버전").ignored() + ) + ) + ) + } } diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt new file mode 100644 index 000000000..2cc4c2596 --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt @@ -0,0 +1,75 @@ +package app.docs.alcohols + +import app.bottlenote.alcohols.domain.DistilleryRepository +import app.bottlenote.alcohols.persentaton.AdminDistilleryController +import app.helper.alcohols.AlcoholsHelper +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.operation.preprocess.Preprocessors.* +import org.springframework.restdocs.payload.JsonFieldType +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.assertj.MockMvcTester + +@WebMvcTest( + controllers = [AdminDistilleryController::class], + excludeAutoConfiguration = [SecurityAutoConfiguration::class] +) +@AutoConfigureRestDocs +@DisplayName("Admin Distillery 컨트롤러 RestDocs 테스트") +class AdminDistilleryControllerDocsTest { + + @Autowired + private lateinit var mvc: MockMvcTester + + @MockitoBean + private lateinit var distilleryRepository: DistilleryRepository + + @Test + @DisplayName("증류소 목록을 조회할 수 있다") + fun getAllDistilleries() { + // given + val items = AlcoholsHelper.createAdminDistilleryItems(3) + + given(distilleryRepository.findAllDistilleries()) + .willReturn(items) + + // when & then + assertThat( + mvc.get().uri("/distilleries") + ) + .hasStatusOk() + .apply( + document( + "admin/distilleries/list", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), + fieldWithPath("data").type(JsonFieldType.ARRAY).description("증류소 목록"), + fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("증류소 ID"), + fieldWithPath("data[].korName").type(JsonFieldType.STRING).description("증류소 한글명"), + fieldWithPath("data[].engName").type(JsonFieldType.STRING).description("증류소 영문명"), + fieldWithPath("data[].logoImgUrl").type(JsonFieldType.STRING).description("로고 이미지 URL"), + fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("생성일시"), + fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일시"), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("API 경로 버전").ignored() + ) + ) + ) + } +} diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt new file mode 100644 index 000000000..648d458bb --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt @@ -0,0 +1,76 @@ +package app.docs.alcohols + +import app.bottlenote.alcohols.domain.RegionRepository +import app.bottlenote.alcohols.persentaton.AdminRegionController +import app.helper.alcohols.AlcoholsHelper +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.operation.preprocess.Preprocessors.* +import org.springframework.restdocs.payload.JsonFieldType +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.assertj.MockMvcTester + +@WebMvcTest( + controllers = [AdminRegionController::class], + excludeAutoConfiguration = [SecurityAutoConfiguration::class] +) +@AutoConfigureRestDocs +@DisplayName("Admin Region 컨트롤러 RestDocs 테스트") +class AdminRegionControllerDocsTest { + + @Autowired + private lateinit var mvc: MockMvcTester + + @MockitoBean + private lateinit var regionRepository: RegionRepository + + @Test + @DisplayName("지역 목록을 조회할 수 있다") + fun getAllRegions() { + // given + val items = AlcoholsHelper.createAdminRegionItems(3) + + given(regionRepository.findAllRegions()) + .willReturn(items) + + // when & then + assertThat( + mvc.get().uri("/regions") + ) + .hasStatusOk() + .apply( + document( + "admin/regions/list", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), + fieldWithPath("data").type(JsonFieldType.ARRAY).description("지역 목록"), + fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("지역 ID"), + fieldWithPath("data[].korName").type(JsonFieldType.STRING).description("국가 한글명"), + fieldWithPath("data[].engName").type(JsonFieldType.STRING).description("국가 영문명"), + fieldWithPath("data[].continent").type(JsonFieldType.STRING).description("대륙"), + fieldWithPath("data[].description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("생성일시"), + fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일시"), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("API 경로 버전").ignored() + ) + ) + ) + } +} diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt new file mode 100644 index 000000000..0c8d6b723 --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt @@ -0,0 +1,76 @@ +package app.docs.alcohols + +import app.bottlenote.alcohols.domain.TastingTagRepository +import app.bottlenote.alcohols.persentaton.AdminTastingTagController +import app.helper.alcohols.AlcoholsHelper +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.operation.preprocess.Preprocessors.* +import org.springframework.restdocs.payload.JsonFieldType +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.assertj.MockMvcTester + +@WebMvcTest( + controllers = [AdminTastingTagController::class], + excludeAutoConfiguration = [SecurityAutoConfiguration::class] +) +@AutoConfigureRestDocs +@DisplayName("Admin TastingTag 컨트롤러 RestDocs 테스트") +class AdminTastingTagControllerDocsTest { + + @Autowired + private lateinit var mvc: MockMvcTester + + @MockitoBean + private lateinit var tastingTagRepository: TastingTagRepository + + @Test + @DisplayName("테이스팅 태그 목록을 조회할 수 있다") + fun getAllTastingTags() { + // given + val items = AlcoholsHelper.createAdminTastingTagItems(3) + + given(tastingTagRepository.findAllTastingTags()) + .willReturn(items) + + // when & then + assertThat( + mvc.get().uri("/tasting-tags") + ) + .hasStatusOk() + .apply( + document( + "admin/tasting-tags/list", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), + fieldWithPath("data").type(JsonFieldType.ARRAY).description("테이스팅 태그 목록"), + fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("태그 ID"), + fieldWithPath("data[].korName").type(JsonFieldType.STRING).description("태그 한글명"), + fieldWithPath("data[].engName").type(JsonFieldType.STRING).description("태그 영문명"), + fieldWithPath("data[].icon").type(JsonFieldType.STRING).description("아이콘"), + fieldWithPath("data[].description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("생성일시"), + fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일시"), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("API 경로 버전").ignored() + ) + ) + ) + } +} diff --git a/bottlenote-admin-api/src/test/kotlin/app/helper/alcohols/AlcoholsHelper.kt b/bottlenote-admin-api/src/test/kotlin/app/helper/alcohols/AlcoholsHelper.kt index 6985dd374..a549b93df 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/helper/alcohols/AlcoholsHelper.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/helper/alcohols/AlcoholsHelper.kt @@ -1,6 +1,11 @@ package app.helper.alcohols +import app.bottlenote.alcohols.dto.response.AdminAlcoholDetailResponse +import app.bottlenote.alcohols.dto.response.AdminAlcoholDetailResponse.TastingTagInfo import app.bottlenote.alcohols.dto.response.AdminAlcoholItem +import app.bottlenote.alcohols.dto.response.AdminDistilleryItem +import app.bottlenote.alcohols.dto.response.AdminRegionItem +import app.bottlenote.alcohols.dto.response.AdminTastingTagItem import app.bottlenote.global.data.response.GlobalResponse import java.time.LocalDateTime @@ -59,4 +64,94 @@ object AlcoholsHelper { ) .build() } + + fun createAdminAlcoholDetailResponse( + alcoholId: Long = 1L, + korName: String = "글렌피딕 12년", + engName: String = "Glenfiddich 12 Year" + ): AdminAlcoholDetailResponse = AdminAlcoholDetailResponse( + alcoholId, + korName, + engName, + "https://example.com/image.jpg", + "WHISKY", + "싱글몰트", + "Single Malt", + "SINGLE_MALT", + "40%", + "12", + "오크", + "700ml", + "스코틀랜드의 대표적인 싱글몰트 위스키", + 1L, + "스코틀랜드", + "Scotland", + 1L, + "글렌피딕", + "Glenfiddich", + listOf( + TastingTagInfo(1L, "바닐라", "Vanilla"), + TastingTagInfo(2L, "꿀", "Honey") + ), + 4.2, + 150L, + 45L, + 200L, + LocalDateTime.of(2024, 1, 1, 0, 0), + LocalDateTime.of(2024, 6, 1, 0, 0) + ) + + fun createAdminTastingTagItems(count: Int = 3): List = + (1..count).map { i -> + AdminTastingTagItem( + i.toLong(), + "태그$i", + "Tag$i", + "icon$i.png", + "테이스팅 태그 설명 $i", + LocalDateTime.of(2024, 1, i, 0, 0), + LocalDateTime.of(2024, 6, i, 0, 0) + ) + } + + fun createAdminRegionItems(count: Int = 3): List = + (1..count).map { i -> + AdminRegionItem( + i.toLong(), + listOf("스코틀랜드", "아일랜드", "일본")[i - 1], + listOf("Scotland", "Ireland", "Japan")[i - 1], + listOf("유럽", "유럽", "아시아")[i - 1], + "지역 설명 $i", + LocalDateTime.of(2024, 1, i, 0, 0), + LocalDateTime.of(2024, 6, i, 0, 0) + ) + } + + fun createAdminDistilleryItems(count: Int = 3): List = + (1..count).map { i -> + AdminDistilleryItem( + i.toLong(), + listOf("글렌피딕", "맥캘란", "야마자키")[i - 1], + listOf("Glenfiddich", "Macallan", "Yamazaki")[i - 1], + "https://example.com/logo$i.png", + LocalDateTime.of(2024, 1, i, 0, 0), + LocalDateTime.of(2024, 6, i, 0, 0) + ) + } + + fun createListResponse(items: List): GlobalResponse = + GlobalResponse.builder() + .success(true) + .code(200) + .data(items) + .errors(emptyList()) + .meta( + mapOf( + "serverVersion" to "1.0.0", + "serverEncoding" to "UTF-8", + "serverResponseTime" to LocalDateTime.now().toString(), + "serverPathVersion" to "v1" + ) + ) + .build() } From 6bb7d98a321d64a1f349366dd23ea484b60f13ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Jan 2026 16:17:01 +0000 Subject: [PATCH 03/14] chore: apply code formatting [skip ci] --- .../bottlenote/alcohols/domain/AlcoholQueryRepository.java | 4 ++-- .../alcohols/repository/CustomAlcoholQueryRepository.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java index 6f70cdd7e..43aaab210 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/AlcoholQueryRepository.java @@ -1,5 +1,7 @@ package app.bottlenote.alcohols.domain; +import static app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; + import app.bottlenote.alcohols.constant.AlcoholType; import app.bottlenote.alcohols.dto.dsl.AlcoholSearchCriteria; import app.bottlenote.alcohols.dto.request.AdminAlcoholSearchRequest; @@ -15,8 +17,6 @@ import org.apache.commons.lang3.tuple.Pair; import org.springframework.data.domain.Page; -import static app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; - /** 알코올 조회 질의에 관한 애그리거트를 정의합니다. */ public interface AlcoholQueryRepository { diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java index 4fd4ce410..2178a09db 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/CustomAlcoholQueryRepository.java @@ -8,10 +8,10 @@ import app.bottlenote.alcohols.facade.payload.AlcoholSummaryItem; import app.bottlenote.global.service.cursor.CursorResponse; import app.bottlenote.global.service.cursor.PageResponse; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.apache.commons.lang3.tuple.Pair; -import java.time.LocalDateTime; import org.springframework.data.domain.Page; public interface CustomAlcoholQueryRepository { From 5ac6f3b4bbe2e90cd71e17771609bde5799c7697 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:30:44 +0000 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20Admin=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20API=EC=97=90=20Pagination=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 테이스팅 태그, 지역, 증류소 목록 API에 페이지네이션 기능을 추가합니다. - AdminReferenceSearchRequest DTO 추가 (keyword, sortOrder, page, size) - TastingTag/Region/Distillery Repository에 Page 반환 및 keyword 검색 지원 - 컨트롤러에서 PageRequest 생성 및 GlobalResponse.fromPage() 사용 - RestDocs 테스트에 queryParameters 및 pagination meta 필드 문서화 - 통합 테스트에 pagination 파라미터 및 meta 검증 추가 --- .../persentaton/AdminDistilleryController.kt | 14 ++++++- .../persentaton/AdminRegionController.kt | 14 ++++++- .../persentaton/AdminTastingTagController.kt | 14 ++++++- .../AdminDistilleryControllerDocsTest.kt | 24 +++++++++-- .../alcohols/AdminRegionControllerDocsTest.kt | 24 +++++++++-- .../AdminTastingTagControllerDocsTest.kt | 24 +++++++++-- .../AdminReferenceDataIntegrationTest.kt | 40 ++++++++++--------- .../alcohols/domain/DistilleryRepository.java | 5 ++- .../alcohols/domain/RegionRepository.java | 4 +- .../alcohols/domain/TastingTagRepository.java | 4 +- .../request/AdminReferenceSearchRequest.java | 25 ++++++++++++ .../repository/JpaDistilleryRepository.java | 11 +++-- .../repository/JpaRegionQueryRepository.java | 10 ++++- .../repository/JpaTastingTagRepository.java | 11 +++-- 14 files changed, 179 insertions(+), 45 deletions(-) create mode 100644 bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt index eda988c71..035b9607d 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt @@ -1,9 +1,13 @@ package app.bottlenote.alcohols.persentaton import app.bottlenote.alcohols.domain.DistilleryRepository +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest import app.bottlenote.global.data.response.GlobalResponse +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -15,7 +19,13 @@ class AdminDistilleryController( ) { @GetMapping - fun getAllDistilleries(): ResponseEntity<*> { - return ResponseEntity.ok(GlobalResponse.ok(distilleryRepository.findAllDistilleries())) + fun getAllDistilleries(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { + val pageable = PageRequest.of( + request.page(), + request.size(), + Sort.by(Sort.Direction.fromString(request.sortOrder().name), "id") + ) + val page = distilleryRepository.findAllDistilleries(request.keyword(), pageable) + return ResponseEntity.ok(GlobalResponse.fromPage(page)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt index ff26d6217..4ae5e1d7c 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt @@ -1,9 +1,13 @@ package app.bottlenote.alcohols.persentaton import app.bottlenote.alcohols.domain.RegionRepository +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest import app.bottlenote.global.data.response.GlobalResponse +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -15,7 +19,13 @@ class AdminRegionController( ) { @GetMapping - fun getAllRegions(): ResponseEntity<*> { - return ResponseEntity.ok(GlobalResponse.ok(regionRepository.findAllRegions())) + fun getAllRegions(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { + val pageable = PageRequest.of( + request.page(), + request.size(), + Sort.by(Sort.Direction.fromString(request.sortOrder().name), "id") + ) + val page = regionRepository.findAllRegions(request.keyword(), pageable) + return ResponseEntity.ok(GlobalResponse.fromPage(page)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt index 6faa0afb4..d8ab38297 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt @@ -1,9 +1,13 @@ package app.bottlenote.alcohols.persentaton import app.bottlenote.alcohols.domain.TastingTagRepository +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest import app.bottlenote.global.data.response.GlobalResponse +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -15,7 +19,13 @@ class AdminTastingTagController( ) { @GetMapping - fun getAllTastingTags(): ResponseEntity<*> { - return ResponseEntity.ok(GlobalResponse.ok(tastingTagRepository.findAllTastingTags())) + fun getAllTastingTags(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { + val pageable = PageRequest.of( + request.page(), + request.size(), + Sort.by(Sort.Direction.fromString(request.sortOrder().name), "id") + ) + val page = tastingTagRepository.findAllTastingTags(request.keyword(), pageable) + return ResponseEntity.ok(GlobalResponse.fromPage(page)) } } diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt index 2cc4c2596..1fdf024df 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt @@ -6,16 +6,22 @@ import app.helper.alcohols.AlcoholsHelper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.restdocs.request.RequestDocumentation.parameterWithName +import org.springframework.restdocs.request.RequestDocumentation.queryParameters import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.assertj.MockMvcTester @@ -38,13 +44,14 @@ class AdminDistilleryControllerDocsTest { fun getAllDistilleries() { // given val items = AlcoholsHelper.createAdminDistilleryItems(3) + val page = PageImpl(items) - given(distilleryRepository.findAllDistilleries()) - .willReturn(items) + given(distilleryRepository.findAllDistilleries(anyString(), any(Pageable::class.java))) + .willReturn(page) // when & then assertThat( - mvc.get().uri("/distilleries") + mvc.get().uri("/distilleries?keyword=&page=0&size=20&sortOrder=ASC") ) .hasStatusOk() .apply( @@ -52,6 +59,12 @@ class AdminDistilleryControllerDocsTest { "admin/distilleries/list", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("keyword").description("검색어 (한글명/영문명)").optional(), + parameterWithName("page").description("페이지 번호 (0부터 시작, 기본값: 0)").optional(), + parameterWithName("size").description("페이지 크기 (기본값: 20)").optional(), + parameterWithName("sortOrder").description("정렬 방향 (ASC/DESC, 기본값: ASC)").optional() + ), responseFields( fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), @@ -64,6 +77,11 @@ class AdminDistilleryControllerDocsTest { fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일시"), fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("meta.size").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("meta.totalElements").type(JsonFieldType.NUMBER).description("전체 요소 수"), + fieldWithPath("meta.totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수"), + fieldWithPath("meta.hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부"), fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt index 648d458bb..51d3a1071 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt @@ -6,16 +6,22 @@ import app.helper.alcohols.AlcoholsHelper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.restdocs.request.RequestDocumentation.parameterWithName +import org.springframework.restdocs.request.RequestDocumentation.queryParameters import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.assertj.MockMvcTester @@ -38,13 +44,14 @@ class AdminRegionControllerDocsTest { fun getAllRegions() { // given val items = AlcoholsHelper.createAdminRegionItems(3) + val page = PageImpl(items) - given(regionRepository.findAllRegions()) - .willReturn(items) + given(regionRepository.findAllRegions(anyString(), any(Pageable::class.java))) + .willReturn(page) // when & then assertThat( - mvc.get().uri("/regions") + mvc.get().uri("/regions?keyword=&page=0&size=20&sortOrder=ASC") ) .hasStatusOk() .apply( @@ -52,6 +59,12 @@ class AdminRegionControllerDocsTest { "admin/regions/list", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("keyword").description("검색어 (한글명/영문명)").optional(), + parameterWithName("page").description("페이지 번호 (0부터 시작, 기본값: 0)").optional(), + parameterWithName("size").description("페이지 크기 (기본값: 20)").optional(), + parameterWithName("sortOrder").description("정렬 방향 (ASC/DESC, 기본값: ASC)").optional() + ), responseFields( fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), @@ -65,6 +78,11 @@ class AdminRegionControllerDocsTest { fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일시"), fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("meta.size").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("meta.totalElements").type(JsonFieldType.NUMBER).description("전체 요소 수"), + fieldWithPath("meta.totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수"), + fieldWithPath("meta.hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부"), fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt index 0c8d6b723..98a4a4c74 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt @@ -6,16 +6,22 @@ import app.helper.alcohols.AlcoholsHelper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.restdocs.request.RequestDocumentation.parameterWithName +import org.springframework.restdocs.request.RequestDocumentation.queryParameters import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.assertj.MockMvcTester @@ -38,13 +44,14 @@ class AdminTastingTagControllerDocsTest { fun getAllTastingTags() { // given val items = AlcoholsHelper.createAdminTastingTagItems(3) + val page = PageImpl(items) - given(tastingTagRepository.findAllTastingTags()) - .willReturn(items) + given(tastingTagRepository.findAllTastingTags(anyString(), any(Pageable::class.java))) + .willReturn(page) // when & then assertThat( - mvc.get().uri("/tasting-tags") + mvc.get().uri("/tasting-tags?keyword=&page=0&size=20&sortOrder=ASC") ) .hasStatusOk() .apply( @@ -52,6 +59,12 @@ class AdminTastingTagControllerDocsTest { "admin/tasting-tags/list", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("keyword").description("검색어 (한글명/영문명)").optional(), + parameterWithName("page").description("페이지 번호 (0부터 시작, 기본값: 0)").optional(), + parameterWithName("size").description("페이지 크기 (기본값: 20)").optional(), + parameterWithName("sortOrder").description("정렬 방향 (ASC/DESC, 기본값: ASC)").optional() + ), responseFields( fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("응답 성공 여부"), fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), @@ -65,6 +78,11 @@ class AdminTastingTagControllerDocsTest { fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일시"), fieldWithPath("errors").type(JsonFieldType.ARRAY).description("에러 목록"), fieldWithPath("meta").type(JsonFieldType.OBJECT).description("메타 정보"), + fieldWithPath("meta.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("meta.size").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("meta.totalElements").type(JsonFieldType.NUMBER).description("전체 요소 수"), + fieldWithPath("meta.totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수"), + fieldWithPath("meta.hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부"), fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("서버 버전").ignored(), fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("서버 인코딩").ignored(), fieldWithPath("meta.serverResponseTime").type(JsonFieldType.STRING).description("서버 응답 시간").ignored(), diff --git a/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt index e5dafed05..3cd4a670e 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt @@ -30,25 +30,25 @@ class AdminReferenceDataIntegrationTest : IntegrationTestSupport() { inner class TastingTagsApi { @Test - @DisplayName("전체 테이스팅 태그 목록을 조회할 수 있다") + @DisplayName("전체 테이스팅 태그 목록을 페이지네이션으로 조회할 수 있다") fun getAllTastingTagsSuccess() { // when & then assertThat( - mockMvcTester.get().uri("/tasting-tags") + mockMvcTester.get().uri("/tasting-tags?page=0&size=20") .header("Authorization", "Bearer $accessToken") ) .hasStatusOk() .bodyJson() .extractingPath("$.success").isEqualTo(true) - // 응답이 리스트 형태임을 확인 + // 페이지네이션 메타 정보 확인 assertThat( - mockMvcTester.get().uri("/tasting-tags") + mockMvcTester.get().uri("/tasting-tags?page=0&size=10") .header("Authorization", "Bearer $accessToken") ) .hasStatusOk() .bodyJson() - .extractingPath("$.data").isNotNull + .extractingPath("$.meta.size").isEqualTo(10) } @Test @@ -67,14 +67,14 @@ class AdminReferenceDataIntegrationTest : IntegrationTestSupport() { inner class RegionsApi { @Test - @DisplayName("전체 지역 목록을 조회할 수 있다") + @DisplayName("전체 지역 목록을 페이지네이션으로 조회할 수 있다") fun getAllRegionsSuccess() { // given - alcoholTestFactory에서 region 데이터가 함께 생성됨 alcoholTestFactory.persistAlcohols(1) // when & then assertThat( - mockMvcTester.get().uri("/regions") + mockMvcTester.get().uri("/regions?page=0&size=20") .header("Authorization", "Bearer $accessToken") ) .hasStatusOk() @@ -83,19 +83,23 @@ class AdminReferenceDataIntegrationTest : IntegrationTestSupport() { } @Test - @DisplayName("지역 목록이 메타 정보를 포함한다") - fun getRegionsWithMetaInfo() { + @DisplayName("지역 목록이 페이지네이션 메타 정보를 포함한다") + fun getRegionsWithPaginationMeta() { // given alcoholTestFactory.persistAlcohols(1) - // when & then - 응답 데이터 확인 - val result = mockMvcTester.get().uri("/regions") + // when & then - 응답 데이터 및 페이지네이션 메타 확인 + val result = mockMvcTester.get().uri("/regions?page=0&size=10") .header("Authorization", "Bearer $accessToken") assertThat(result) .hasStatusOk() .bodyJson() - .extractingPath("$.data").isNotNull + .extractingPath("$.meta.page").isEqualTo(0) + + assertThat(result) + .bodyJson() + .extractingPath("$.meta.totalElements").isNotNull // 방어로직: 인증 없이 요청 시 실패 assertThat( @@ -110,14 +114,14 @@ class AdminReferenceDataIntegrationTest : IntegrationTestSupport() { inner class DistilleriesApi { @Test - @DisplayName("전체 증류소 목록을 조회할 수 있다") + @DisplayName("전체 증류소 목록을 페이지네이션으로 조회할 수 있다") fun getAllDistilleriesSuccess() { // given - alcoholTestFactory에서 distillery 데이터가 함께 생성됨 alcoholTestFactory.persistAlcohols(1) // when & then assertThat( - mockMvcTester.get().uri("/distilleries") + mockMvcTester.get().uri("/distilleries?page=0&size=20") .header("Authorization", "Bearer $accessToken") ) .hasStatusOk() @@ -126,13 +130,13 @@ class AdminReferenceDataIntegrationTest : IntegrationTestSupport() { } @Test - @DisplayName("증류소 목록이 필수 필드를 포함한다") - fun getDistilleriesWithRequiredFields() { + @DisplayName("증류소 목록을 키워드로 검색할 수 있다") + fun getDistilleriesWithKeyword() { // given alcoholTestFactory.persistAlcohols(1) - // when & then - val result = mockMvcTester.get().uri("/distilleries") + // when & then - 키워드 검색 + val result = mockMvcTester.get().uri("/distilleries?keyword=&page=0&size=20") .header("Authorization", "Bearer $accessToken") assertThat(result) diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java index 902f4c8ff..e43d2cd38 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java @@ -1,9 +1,10 @@ package app.bottlenote.alcohols.domain; import app.bottlenote.alcohols.dto.response.AdminDistilleryItem; -import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface DistilleryRepository { - List findAllDistilleries(); + Page findAllDistilleries(String keyword, Pageable pageable); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java index 796250fdf..276f14b0a 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/RegionRepository.java @@ -3,10 +3,12 @@ import app.bottlenote.alcohols.dto.response.AdminRegionItem; import app.bottlenote.alcohols.dto.response.RegionsItem; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface RegionRepository { List findAllRegionsResponse(); - List findAllRegions(); + Page findAllRegions(String keyword, Pageable pageable); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java index a28625d5f..34953cfa4 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/TastingTagRepository.java @@ -2,10 +2,12 @@ import app.bottlenote.alcohols.dto.response.AdminTastingTagItem; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface TastingTagRepository { List findAll(); - List findAllTastingTags(); + Page findAllTastingTags(String keyword, Pageable pageable); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java new file mode 100644 index 000000000..90d05f9fd --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java @@ -0,0 +1,25 @@ +package app.bottlenote.alcohols.dto.request; + +import app.bottlenote.global.service.cursor.SortOrder; +import lombok.Builder; + +/** + * 참조 데이터 (테이스팅 태그, 지역, 증류소) 검색용 공통 Request + * + * @param keyword 검색어 + * @param sortOrder 정렬 방향 + * @param page 페이지 번호 (0부터) + * @param size 페이지 크기 + */ +public record AdminReferenceSearchRequest( + String keyword, + SortOrder sortOrder, + Integer page, + Integer size) { + @Builder + public AdminReferenceSearchRequest { + sortOrder = sortOrder != null ? sortOrder : SortOrder.ASC; + page = page != null ? page : 0; + size = size != null ? size : 20; + } +} diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java index 9b8fe4686..0e701c14a 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java @@ -4,9 +4,11 @@ import app.bottlenote.alcohols.domain.DistilleryRepository; import app.bottlenote.alcohols.dto.response.AdminDistilleryItem; import app.bottlenote.common.annotation.JpaRepositoryImpl; -import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; @JpaRepositoryImpl public interface JpaDistilleryRepository @@ -18,7 +20,10 @@ public interface JpaDistilleryRepository select new app.bottlenote.alcohols.dto.response.AdminDistilleryItem( d.id, d.korName, d.engName, d.logoImgPath, d.createAt, d.lastModifyAt ) - from distillery d order by d.id asc + from distillery d + where (:keyword is null or :keyword = '' + or d.korName like concat('%', :keyword, '%') + or d.engName like concat('%', :keyword, '%')) """) - List findAllDistilleries(); + Page findAllDistilleries(@Param("keyword") String keyword, Pageable pageable); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java index 449652815..ec49e9b62 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaRegionQueryRepository.java @@ -6,8 +6,11 @@ import app.bottlenote.alcohols.dto.response.RegionsItem; import app.bottlenote.common.annotation.JpaRepositoryImpl; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; @JpaRepositoryImpl public interface JpaRegionQueryRepository extends RegionRepository, CrudRepository { @@ -26,7 +29,10 @@ public interface JpaRegionQueryRepository extends RegionRepository, CrudReposito select new app.bottlenote.alcohols.dto.response.AdminRegionItem( r.id, r.korName, r.engName, r.continent, r.description, r.createAt, r.lastModifyAt ) - from region r order by r.id asc + from region r + where (:keyword is null or :keyword = '' + or r.korName like concat('%', :keyword, '%') + or r.engName like concat('%', :keyword, '%')) """) - List findAllRegions(); + Page findAllRegions(@Param("keyword") String keyword, Pageable pageable); } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java index 62f4032ad..f1c343aed 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaTastingTagRepository.java @@ -4,9 +4,11 @@ import app.bottlenote.alcohols.domain.TastingTagRepository; import app.bottlenote.alcohols.dto.response.AdminTastingTagItem; import app.bottlenote.common.annotation.JpaRepositoryImpl; -import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; @JpaRepositoryImpl public interface JpaTastingTagRepository @@ -18,7 +20,10 @@ public interface JpaTastingTagRepository select new app.bottlenote.alcohols.dto.response.AdminTastingTagItem( t.id, t.korName, t.engName, t.icon, t.description, t.createAt, t.lastModifyAt ) - from tasting_tag t order by t.id asc + from tasting_tag t + where (:keyword is null or :keyword = '' + or t.korName like concat('%', :keyword, '%') + or t.engName like concat('%', :keyword, '%')) """) - List findAllTastingTags(); + Page findAllTastingTags(@Param("keyword") String keyword, Pageable pageable); } From 0778258b85e837a1fdc7849bc866ef182ef3776f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Jan 2026 16:36:43 +0000 Subject: [PATCH 05/14] chore: apply code formatting [skip ci] --- .../alcohols/dto/request/AdminReferenceSearchRequest.java | 5 +---- .../alcohols/repository/JpaDistilleryRepository.java | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java index 90d05f9fd..10b68ed97 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java @@ -12,10 +12,7 @@ * @param size 페이지 크기 */ public record AdminReferenceSearchRequest( - String keyword, - SortOrder sortOrder, - Integer page, - Integer size) { + String keyword, SortOrder sortOrder, Integer page, Integer size) { @Builder public AdminReferenceSearchRequest { sortOrder = sortOrder != null ? sortOrder : SortOrder.ASC; diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java index 0e701c14a..16f4d35eb 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java @@ -25,5 +25,6 @@ public interface JpaDistilleryRepository or d.korName like concat('%', :keyword, '%') or d.engName like concat('%', :keyword, '%')) """) - Page findAllDistilleries(@Param("keyword") String keyword, Pageable pageable); + Page findAllDistilleries( + @Param("keyword") String keyword, Pageable pageable); } From dcdc477d6831b115f7e527fcd13f3d205e803db5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:38:47 +0000 Subject: [PATCH 06/14] =?UTF-8?q?fix:=20InMemoryTastingTagRepository?= =?UTF-8?q?=EC=97=90=20=EB=88=84=EB=9D=BD=EB=90=9C=20findAllTastingTags=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TastingTagRepository 인터페이스에 Pagination 메서드 추가 후 InMemory 구현체에 누락된 메서드 구현 --- .../fixture/InMemoryTastingTagRepository.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java index ce3837b94..601339a1c 100644 --- a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java +++ b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java @@ -2,9 +2,13 @@ import app.bottlenote.alcohols.domain.TastingTag; import app.bottlenote.alcohols.domain.TastingTagRepository; +import app.bottlenote.alcohols.dto.response.AdminTastingTagItem; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.test.util.ReflectionTestUtils; public class InMemoryTastingTagRepository implements TastingTagRepository { @@ -16,6 +20,27 @@ public List findAll() { return List.copyOf(tags); } + @Override + public Page findAllTastingTags(String keyword, Pageable pageable) { + List filtered = tags.stream() + .filter(t -> keyword == null || keyword.isEmpty() + || t.getKorName().contains(keyword) + || t.getEngName().contains(keyword)) + .map(t -> new AdminTastingTagItem( + t.getId(), t.getKorName(), t.getEngName(), + t.getIcon(), t.getDescription(), + t.getCreateAt(), t.getLastModifyAt())) + .toList(); + + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), filtered.size()); + List pageContent = start < filtered.size() + ? filtered.subList(start, end) + : List.of(); + + return new PageImpl<>(pageContent, pageable, filtered.size()); + } + public TastingTag save(TastingTag tag) { Long id = tag.getId(); if (Objects.isNull(id)) { From 9d6f67a40000baaabde85f1b223a75c7686309cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Jan 2026 16:40:00 +0000 Subject: [PATCH 07/14] chore: apply code formatting [skip ci] --- .../fixture/InMemoryTastingTagRepository.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java index 601339a1c..2f8b44f4c 100644 --- a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java +++ b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryTastingTagRepository.java @@ -22,21 +22,30 @@ public List findAll() { @Override public Page findAllTastingTags(String keyword, Pageable pageable) { - List filtered = tags.stream() - .filter(t -> keyword == null || keyword.isEmpty() - || t.getKorName().contains(keyword) - || t.getEngName().contains(keyword)) - .map(t -> new AdminTastingTagItem( - t.getId(), t.getKorName(), t.getEngName(), - t.getIcon(), t.getDescription(), - t.getCreateAt(), t.getLastModifyAt())) - .toList(); + List filtered = + tags.stream() + .filter( + t -> + keyword == null + || keyword.isEmpty() + || t.getKorName().contains(keyword) + || t.getEngName().contains(keyword)) + .map( + t -> + new AdminTastingTagItem( + t.getId(), + t.getKorName(), + t.getEngName(), + t.getIcon(), + t.getDescription(), + t.getCreateAt(), + t.getLastModifyAt())) + .toList(); int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), filtered.size()); - List pageContent = start < filtered.size() - ? filtered.subList(start, end) - : List.of(); + List pageContent = + start < filtered.size() ? filtered.subList(start, end) : List.of(); return new PageImpl<>(pageContent, pageable, filtered.size()); } From 9523479326b0778c84f446ff3ca1507a3dc439de Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:44:25 +0000 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20CI=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin-api에 spring-boot-starter-data-jpa 의존성 추가 (main scope) - InMemoryAlcoholQueryRepository에 누락된 findAdminAlcoholDetailById 메서드 추가 --- bottlenote-admin-api/build.gradle.kts | 3 +++ .../alcohols/fixture/InMemoryAlcoholQueryRepository.java | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/bottlenote-admin-api/build.gradle.kts b/bottlenote-admin-api/build.gradle.kts index ff85cee23..2443255db 100644 --- a/bottlenote-admin-api/build.gradle.kts +++ b/bottlenote-admin-api/build.gradle.kts @@ -18,6 +18,9 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + // Spring Data + implementation(libs.spring.boot.starter.data.jpa) + // Security implementation(libs.spring.boot.starter.security) implementation(libs.spring.security.test) diff --git a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java index dee5d4697..27bfc6ef6 100644 --- a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java +++ b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java @@ -9,6 +9,7 @@ import app.bottlenote.alcohols.dto.response.AlcoholDetailItem; import app.bottlenote.alcohols.dto.response.AlcoholSearchResponse; import app.bottlenote.alcohols.dto.response.CategoryItem; +import app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; import app.bottlenote.alcohols.facade.payload.AlcoholSummaryItem; import app.bottlenote.global.service.cursor.CursorResponse; import app.bottlenote.global.service.cursor.PageResponse; @@ -87,4 +88,9 @@ public Pair> getStandardExplore( public Page searchAdminAlcohols(AdminAlcoholSearchRequest request) { return new PageImpl<>(List.of()); } + + @Override + public Optional findAdminAlcoholDetailById(Long alcoholId) { + return Optional.empty(); + } } From acf502988fb88d0afccc6aad1dc6dacbcac728de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Jan 2026 16:45:24 +0000 Subject: [PATCH 09/14] chore: apply code formatting [skip ci] --- .../alcohols/fixture/InMemoryAlcoholQueryRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java index 27bfc6ef6..53532ec6b 100644 --- a/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java +++ b/bottlenote-mono/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java @@ -9,8 +9,8 @@ import app.bottlenote.alcohols.dto.response.AlcoholDetailItem; import app.bottlenote.alcohols.dto.response.AlcoholSearchResponse; import app.bottlenote.alcohols.dto.response.CategoryItem; -import app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; import app.bottlenote.alcohols.facade.payload.AlcoholSummaryItem; +import app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; import app.bottlenote.global.service.cursor.CursorResponse; import app.bottlenote.global.service.cursor.PageResponse; import java.util.HashMap; From 347d9049308912928359506c454fb59703781f06 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:48:57 +0000 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20product-api=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin-api에서 spring-data-jpa 의존성 제거 (testImplementation만 유지) - AdminReferenceSearchRequest에 toPageable() 메서드 추가 - 컨트롤러에서 PageRequest, Sort import 제거하고 toPageable() 사용 --- bottlenote-admin-api/build.gradle.kts | 3 --- .../alcohols/persentaton/AdminDistilleryController.kt | 9 +-------- .../alcohols/persentaton/AdminRegionController.kt | 9 +-------- .../alcohols/persentaton/AdminTastingTagController.kt | 9 +-------- .../dto/request/AdminReferenceSearchRequest.java | 8 ++++++++ 5 files changed, 11 insertions(+), 27 deletions(-) diff --git a/bottlenote-admin-api/build.gradle.kts b/bottlenote-admin-api/build.gradle.kts index 2443255db..ff85cee23 100644 --- a/bottlenote-admin-api/build.gradle.kts +++ b/bottlenote-admin-api/build.gradle.kts @@ -18,9 +18,6 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") - // Spring Data - implementation(libs.spring.boot.starter.data.jpa) - // Security implementation(libs.spring.boot.starter.security) implementation(libs.spring.security.test) diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt index 035b9607d..34c9fae03 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt @@ -3,8 +3,6 @@ package app.bottlenote.alcohols.persentaton import app.bottlenote.alcohols.domain.DistilleryRepository import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest import app.bottlenote.global.data.response.GlobalResponse -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute @@ -20,12 +18,7 @@ class AdminDistilleryController( @GetMapping fun getAllDistilleries(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { - val pageable = PageRequest.of( - request.page(), - request.size(), - Sort.by(Sort.Direction.fromString(request.sortOrder().name), "id") - ) - val page = distilleryRepository.findAllDistilleries(request.keyword(), pageable) + val page = distilleryRepository.findAllDistilleries(request.keyword(), request.toPageable()) return ResponseEntity.ok(GlobalResponse.fromPage(page)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt index 4ae5e1d7c..e06a9c7c2 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt @@ -3,8 +3,6 @@ package app.bottlenote.alcohols.persentaton import app.bottlenote.alcohols.domain.RegionRepository import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest import app.bottlenote.global.data.response.GlobalResponse -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute @@ -20,12 +18,7 @@ class AdminRegionController( @GetMapping fun getAllRegions(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { - val pageable = PageRequest.of( - request.page(), - request.size(), - Sort.by(Sort.Direction.fromString(request.sortOrder().name), "id") - ) - val page = regionRepository.findAllRegions(request.keyword(), pageable) + val page = regionRepository.findAllRegions(request.keyword(), request.toPageable()) return ResponseEntity.ok(GlobalResponse.fromPage(page)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt index d8ab38297..928aa8dae 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt @@ -3,8 +3,6 @@ package app.bottlenote.alcohols.persentaton import app.bottlenote.alcohols.domain.TastingTagRepository import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest import app.bottlenote.global.data.response.GlobalResponse -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute @@ -20,12 +18,7 @@ class AdminTastingTagController( @GetMapping fun getAllTastingTags(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { - val pageable = PageRequest.of( - request.page(), - request.size(), - Sort.by(Sort.Direction.fromString(request.sortOrder().name), "id") - ) - val page = tastingTagRepository.findAllTastingTags(request.keyword(), pageable) + val page = tastingTagRepository.findAllTastingTags(request.keyword(), request.toPageable()) return ResponseEntity.ok(GlobalResponse.fromPage(page)) } } diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java index 10b68ed97..9cbf4db7f 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java @@ -2,6 +2,9 @@ import app.bottlenote.global.service.cursor.SortOrder; import lombok.Builder; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; /** * 참조 데이터 (테이스팅 태그, 지역, 증류소) 검색용 공통 Request @@ -19,4 +22,9 @@ public record AdminReferenceSearchRequest( page = page != null ? page : 0; size = size != null ? size : 20; } + + public Pageable toPageable() { + return PageRequest.of( + page, size, Sort.by(Sort.Direction.fromString(sortOrder.name()), "id")); + } } From 6544b4a6c3f17e7719322eb783a9f3eacaa562de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Jan 2026 16:49:53 +0000 Subject: [PATCH 11/14] chore: apply code formatting [skip ci] --- .../alcohols/dto/request/AdminReferenceSearchRequest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java index 9cbf4db7f..2bd360dfc 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java @@ -24,7 +24,6 @@ public record AdminReferenceSearchRequest( } public Pageable toPageable() { - return PageRequest.of( - page, size, Sort.by(Sort.Direction.fromString(sortOrder.name()), "id")); + return PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(sortOrder.name()), "id")); } } From 20dc2b605fbde5eab6972b51c9d0bb77249f4048 Mon Sep 17 00:00:00 2001 From: rlagu Date: Tue, 20 Jan 2026 02:29:09 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20auth=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=AA=85=20persentaton=20=E2=86=92=20presentation=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .../auth/{persentaton => presentation}/AuthController.kt | 2 +- .../src/test/kotlin/app/docs/auth/AuthControllerDocsTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/{persentaton => presentation}/AuthController.kt (98%) diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/persentaton/AuthController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/presentation/AuthController.kt similarity index 98% rename from bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/persentaton/AuthController.kt rename to bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/presentation/AuthController.kt index c9824ba59..71e79302a 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/persentaton/AuthController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/auth/presentation/AuthController.kt @@ -1,4 +1,4 @@ -package app.bottlenote.auth.persentaton +package app.bottlenote.auth.presentation import app.bottlenote.auth.config.RootAdminProperties import app.bottlenote.global.data.response.GlobalResponse diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/auth/AuthControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/auth/AuthControllerDocsTest.kt index 61e987371..b1bd14ea1 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/auth/AuthControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/auth/AuthControllerDocsTest.kt @@ -1,7 +1,7 @@ package app.docs.auth import app.bottlenote.auth.config.RootAdminProperties -import app.bottlenote.auth.persentaton.AuthController +import app.bottlenote.auth.presentation.AuthController import app.bottlenote.global.security.SecurityContextUtil import app.bottlenote.user.constant.AdminRole import app.bottlenote.user.dto.request.AdminSignupRequest From c95d6cfb30b07a52f19f1223ed2f7f166d8fb566 Mon Sep 17 00:00:00 2001 From: rlagu Date: Tue, 20 Jan 2026 02:29:57 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20alcohols=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=AA=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20Service?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .../AdminAlcoholsController.kt | 4 ++-- .../AdminDistilleryController.kt | 9 ++++--- .../AdminRegionController.kt | 9 ++++--- .../AdminTastingTagController.kt | 9 ++++--- .../AdminAlcoholsControllerDocsTest.kt | 7 ++---- .../AdminDistilleryControllerDocsTest.kt | 15 ++++++------ .../alcohols/AdminRegionControllerDocsTest.kt | 15 ++++++------ .../AdminTastingTagControllerDocsTest.kt | 15 ++++++------ .../service/AlcoholReferenceService.java | 24 +++++++++++++++++++ git.environment-variables | 2 +- 10 files changed, 65 insertions(+), 44 deletions(-) rename bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/{persentaton => presentation}/AdminAlcoholsController.kt (86%) rename bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/{persentaton => presentation}/AdminDistilleryController.kt (66%) rename bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/{persentaton => presentation}/AdminRegionController.kt (67%) rename bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/{persentaton => presentation}/AdminTastingTagController.kt (66%) diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminAlcoholsController.kt similarity index 86% rename from bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt rename to bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminAlcoholsController.kt index db8a37305..b6bb2a87c 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminAlcoholsController.kt @@ -1,4 +1,4 @@ -package app.bottlenote.alcohols.persentaton +package app.bottlenote.alcohols.presentation import app.bottlenote.alcohols.dto.request.AdminAlcoholSearchRequest import app.bottlenote.alcohols.service.AlcoholQueryService @@ -24,6 +24,6 @@ class AdminAlcoholsController( @GetMapping("/{alcoholId}") fun getAlcoholDetail(@PathVariable alcoholId: Long): ResponseEntity<*> { - return ResponseEntity.ok(GlobalResponse.ok(alcoholQueryService.findAdminAlcoholDetailById(alcoholId))) + return GlobalResponse.ok(alcoholQueryService.findAdminAlcoholDetailById(alcoholId)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminDistilleryController.kt similarity index 66% rename from bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt rename to bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminDistilleryController.kt index 34c9fae03..60dd68818 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminDistilleryController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminDistilleryController.kt @@ -1,7 +1,7 @@ -package app.bottlenote.alcohols.persentaton +package app.bottlenote.alcohols.presentation -import app.bottlenote.alcohols.domain.DistilleryRepository import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest +import app.bottlenote.alcohols.service.AlcoholReferenceService import app.bottlenote.global.data.response.GlobalResponse import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -13,12 +13,11 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/distilleries") class AdminDistilleryController( - private val distilleryRepository: DistilleryRepository + private val alcoholReferenceService: AlcoholReferenceService ) { @GetMapping fun getAllDistilleries(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { - val page = distilleryRepository.findAllDistilleries(request.keyword(), request.toPageable()) - return ResponseEntity.ok(GlobalResponse.fromPage(page)) + return ResponseEntity.ok(alcoholReferenceService.findAllDistilleries(request)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminRegionController.kt similarity index 67% rename from bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt rename to bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminRegionController.kt index e06a9c7c2..b67ce25c2 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminRegionController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminRegionController.kt @@ -1,7 +1,7 @@ -package app.bottlenote.alcohols.persentaton +package app.bottlenote.alcohols.presentation -import app.bottlenote.alcohols.domain.RegionRepository import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest +import app.bottlenote.alcohols.service.AlcoholReferenceService import app.bottlenote.global.data.response.GlobalResponse import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -13,12 +13,11 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/regions") class AdminRegionController( - private val regionRepository: RegionRepository + private val alcoholReferenceService: AlcoholReferenceService ) { @GetMapping fun getAllRegions(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { - val page = regionRepository.findAllRegions(request.keyword(), request.toPageable()) - return ResponseEntity.ok(GlobalResponse.fromPage(page)) + return ResponseEntity.ok(alcoholReferenceService.findAllRegionsForAdmin(request)) } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminTastingTagController.kt similarity index 66% rename from bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt rename to bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminTastingTagController.kt index 928aa8dae..a6077d061 100644 --- a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/persentaton/AdminTastingTagController.kt +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminTastingTagController.kt @@ -1,7 +1,7 @@ -package app.bottlenote.alcohols.persentaton +package app.bottlenote.alcohols.presentation -import app.bottlenote.alcohols.domain.TastingTagRepository import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest +import app.bottlenote.alcohols.service.AlcoholReferenceService import app.bottlenote.global.data.response.GlobalResponse import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -13,12 +13,11 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/tasting-tags") class AdminTastingTagController( - private val tastingTagRepository: TastingTagRepository + private val alcoholReferenceService: AlcoholReferenceService ) { @GetMapping fun getAllTastingTags(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { - val page = tastingTagRepository.findAllTastingTags(request.keyword(), request.toPageable()) - return ResponseEntity.ok(GlobalResponse.fromPage(page)) + return ResponseEntity.ok(alcoholReferenceService.findAllTastingTags(request)) } } diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt index a75ae00bb..f67ae168d 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt @@ -3,8 +3,7 @@ package app.docs.alcohols import app.bottlenote.alcohols.constant.AdminAlcoholSortType import app.bottlenote.alcohols.constant.AlcoholCategoryGroup import app.bottlenote.alcohols.dto.request.AdminAlcoholSearchRequest -import app.bottlenote.alcohols.dto.response.AdminAlcoholDetailResponse -import app.bottlenote.alcohols.persentaton.AdminAlcoholsController +import app.bottlenote.alcohols.presentation.AdminAlcoholsController import app.bottlenote.alcohols.service.AlcoholQueryService import app.bottlenote.global.service.cursor.SortOrder import app.helper.alcohols.AlcoholsHelper @@ -23,9 +22,7 @@ import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.responseFields -import org.springframework.restdocs.request.RequestDocumentation.parameterWithName -import org.springframework.restdocs.request.RequestDocumentation.pathParameters -import org.springframework.restdocs.request.RequestDocumentation.queryParameters +import org.springframework.restdocs.request.RequestDocumentation.* import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.assertj.MockMvcTester diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt index 1fdf024df..410ee27a1 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt @@ -1,20 +1,20 @@ package app.docs.alcohols -import app.bottlenote.alcohols.domain.DistilleryRepository -import app.bottlenote.alcohols.persentaton.AdminDistilleryController +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest +import app.bottlenote.alcohols.presentation.AdminDistilleryController +import app.bottlenote.alcohols.service.AlcoholReferenceService +import app.bottlenote.global.data.response.GlobalResponse import app.helper.alcohols.AlcoholsHelper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyString import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.data.domain.PageImpl -import org.springframework.data.domain.Pageable import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType @@ -37,7 +37,7 @@ class AdminDistilleryControllerDocsTest { private lateinit var mvc: MockMvcTester @MockitoBean - private lateinit var distilleryRepository: DistilleryRepository + private lateinit var alcoholReferenceService: AlcoholReferenceService @Test @DisplayName("증류소 목록을 조회할 수 있다") @@ -45,9 +45,10 @@ class AdminDistilleryControllerDocsTest { // given val items = AlcoholsHelper.createAdminDistilleryItems(3) val page = PageImpl(items) + val response = GlobalResponse.fromPage(page) - given(distilleryRepository.findAllDistilleries(anyString(), any(Pageable::class.java))) - .willReturn(page) + given(alcoholReferenceService.findAllDistilleries(any(AdminReferenceSearchRequest::class.java))) + .willReturn(response) // when & then assertThat( diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt index 51d3a1071..442ff0d0d 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt @@ -1,20 +1,20 @@ package app.docs.alcohols -import app.bottlenote.alcohols.domain.RegionRepository -import app.bottlenote.alcohols.persentaton.AdminRegionController +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest +import app.bottlenote.alcohols.presentation.AdminRegionController +import app.bottlenote.alcohols.service.AlcoholReferenceService +import app.bottlenote.global.data.response.GlobalResponse import app.helper.alcohols.AlcoholsHelper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyString import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.data.domain.PageImpl -import org.springframework.data.domain.Pageable import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType @@ -37,7 +37,7 @@ class AdminRegionControllerDocsTest { private lateinit var mvc: MockMvcTester @MockitoBean - private lateinit var regionRepository: RegionRepository + private lateinit var alcoholReferenceService: AlcoholReferenceService @Test @DisplayName("지역 목록을 조회할 수 있다") @@ -45,9 +45,10 @@ class AdminRegionControllerDocsTest { // given val items = AlcoholsHelper.createAdminRegionItems(3) val page = PageImpl(items) + val response = GlobalResponse.fromPage(page) - given(regionRepository.findAllRegions(anyString(), any(Pageable::class.java))) - .willReturn(page) + given(alcoholReferenceService.findAllRegionsForAdmin(any(AdminReferenceSearchRequest::class.java))) + .willReturn(response) // when & then assertThat( diff --git a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt index 98a4a4c74..351dbff82 100644 --- a/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt @@ -1,20 +1,20 @@ package app.docs.alcohols -import app.bottlenote.alcohols.domain.TastingTagRepository -import app.bottlenote.alcohols.persentaton.AdminTastingTagController +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest +import app.bottlenote.alcohols.presentation.AdminTastingTagController +import app.bottlenote.alcohols.service.AlcoholReferenceService +import app.bottlenote.global.data.response.GlobalResponse import app.helper.alcohols.AlcoholsHelper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyString import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.data.domain.PageImpl -import org.springframework.data.domain.Pageable import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.JsonFieldType @@ -37,7 +37,7 @@ class AdminTastingTagControllerDocsTest { private lateinit var mvc: MockMvcTester @MockitoBean - private lateinit var tastingTagRepository: TastingTagRepository + private lateinit var alcoholReferenceService: AlcoholReferenceService @Test @DisplayName("테이스팅 태그 목록을 조회할 수 있다") @@ -45,9 +45,10 @@ class AdminTastingTagControllerDocsTest { // given val items = AlcoholsHelper.createAdminTastingTagItems(3) val page = PageImpl(items) + val response = GlobalResponse.fromPage(page) - given(tastingTagRepository.findAllTastingTags(anyString(), any(Pageable::class.java))) - .willReturn(page) + given(alcoholReferenceService.findAllTastingTags(any(AdminReferenceSearchRequest::class.java))) + .willReturn(response) // when & then assertThat( diff --git a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholReferenceService.java b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholReferenceService.java index 5ac9cb3cc..855760ed8 100644 --- a/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholReferenceService.java +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/service/AlcoholReferenceService.java @@ -5,12 +5,16 @@ import app.bottlenote.alcohols.constant.AlcoholType; import app.bottlenote.alcohols.domain.AlcoholQueryRepository; import app.bottlenote.alcohols.domain.CurationKeywordRepository; +import app.bottlenote.alcohols.domain.DistilleryRepository; import app.bottlenote.alcohols.domain.RegionRepository; +import app.bottlenote.alcohols.domain.TastingTagRepository; +import app.bottlenote.alcohols.dto.request.AdminReferenceSearchRequest; import app.bottlenote.alcohols.dto.request.CurationKeywordSearchRequest; import app.bottlenote.alcohols.dto.response.AlcoholsSearchItem; import app.bottlenote.alcohols.dto.response.CategoryItem; import app.bottlenote.alcohols.dto.response.CurationKeywordResponse; import app.bottlenote.alcohols.dto.response.RegionsItem; +import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.service.cursor.CursorResponse; import java.util.List; import java.util.Optional; @@ -28,6 +32,8 @@ public class AlcoholReferenceService { private final RegionRepository regionQueryRepository; private final AlcoholQueryRepository alcoholQueryRepository; private final CurationKeywordRepository curationKeywordRepository; + private final DistilleryRepository distilleryRepository; + private final TastingTagRepository tastingTagRepository; @Cacheable(value = "local_cache_alcohol_region_information") @Transactional(readOnly = true) @@ -64,4 +70,22 @@ public Optional> getCurationAlcoholIds(String keyword) { public Optional> getCurationAlcoholIds(Long curationId) { return curationKeywordRepository.findById(curationId).map(curation -> curation.getAlcoholIds()); } + + @Transactional(readOnly = true) + public GlobalResponse findAllRegionsForAdmin(AdminReferenceSearchRequest request) { + return GlobalResponse.fromPage( + regionQueryRepository.findAllRegions(request.keyword(), request.toPageable())); + } + + @Transactional(readOnly = true) + public GlobalResponse findAllDistilleries(AdminReferenceSearchRequest request) { + return GlobalResponse.fromPage( + distilleryRepository.findAllDistilleries(request.keyword(), request.toPageable())); + } + + @Transactional(readOnly = true) + public GlobalResponse findAllTastingTags(AdminReferenceSearchRequest request) { + return GlobalResponse.fromPage( + tastingTagRepository.findAllTastingTags(request.keyword(), request.toPageable())); + } } diff --git a/git.environment-variables b/git.environment-variables index 416ba9202..daab1fbf4 160000 --- a/git.environment-variables +++ b/git.environment-variables @@ -1 +1 @@ -Subproject commit 416ba92027f741481ff86cbece1c3b6b397e13a2 +Subproject commit daab1fbf413663320cd37ed183abdb80b7efe0a5 From d16c6c43b3cf4cf6eb772b6f0643f655b0f933a5 Mon Sep 17 00:00:00 2001 From: rlagu Date: Tue, 20 Jan 2026 02:38:30 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20InMemoryAlcoholQueryRepository?= =?UTF-8?q?=EC=97=90=20=EB=88=84=EB=9D=BD=EB=90=9C=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .../alcohols/fixture/InMemoryAlcoholQueryRepository.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bottlenote-product-api/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java b/bottlenote-product-api/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java index 62b62f096..ede921a9f 100644 --- a/bottlenote-product-api/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java +++ b/bottlenote-product-api/src/test/java/app/bottlenote/alcohols/fixture/InMemoryAlcoholQueryRepository.java @@ -10,6 +10,7 @@ import app.bottlenote.alcohols.dto.response.AlcoholSearchResponse; import app.bottlenote.alcohols.dto.response.CategoryItem; import app.bottlenote.alcohols.facade.payload.AlcoholSummaryItem; +import app.bottlenote.alcohols.repository.CustomAlcoholQueryRepository.AdminAlcoholDetailProjection; import app.bottlenote.global.service.cursor.CursorResponse; import app.bottlenote.global.service.cursor.PageResponse; import java.util.HashMap; @@ -78,4 +79,9 @@ public Pair> getStandardExplore( public Page searchAdminAlcohols(AdminAlcoholSearchRequest request) { return Page.empty(); } + + @Override + public Optional findAdminAlcoholDetailById(Long alcoholId) { + return Optional.empty(); + } }