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/main/kotlin/app/bottlenote/alcohols/persentaton/AdminAlcoholsController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminAlcoholsController.kt similarity index 73% 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 c9f98a6ed..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 @@ -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 GlobalResponse.ok(alcoholQueryService.findAdminAlcoholDetailById(alcoholId)) + } } diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminDistilleryController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminDistilleryController.kt new file mode 100644 index 000000000..60dd68818 --- /dev/null +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminDistilleryController.kt @@ -0,0 +1,23 @@ +package app.bottlenote.alcohols.presentation + +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 +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/distilleries") +class AdminDistilleryController( + private val alcoholReferenceService: AlcoholReferenceService +) { + + @GetMapping + fun getAllDistilleries(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { + return ResponseEntity.ok(alcoholReferenceService.findAllDistilleries(request)) + } +} diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminRegionController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminRegionController.kt new file mode 100644 index 000000000..b67ce25c2 --- /dev/null +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminRegionController.kt @@ -0,0 +1,23 @@ +package app.bottlenote.alcohols.presentation + +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 +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/regions") +class AdminRegionController( + private val alcoholReferenceService: AlcoholReferenceService +) { + + @GetMapping + fun getAllRegions(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { + return ResponseEntity.ok(alcoholReferenceService.findAllRegionsForAdmin(request)) + } +} diff --git a/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminTastingTagController.kt b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminTastingTagController.kt new file mode 100644 index 000000000..a6077d061 --- /dev/null +++ b/bottlenote-admin-api/src/main/kotlin/app/bottlenote/alcohols/presentation/AdminTastingTagController.kt @@ -0,0 +1,23 @@ +package app.bottlenote.alcohols.presentation + +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 +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/tasting-tags") +class AdminTastingTagController( + private val alcoholReferenceService: AlcoholReferenceService +) { + + @GetMapping + fun getAllTastingTags(@ModelAttribute request: AdminReferenceSearchRequest): ResponseEntity<*> { + return ResponseEntity.ok(alcoholReferenceService.findAllTastingTags(request)) + } +} 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/alcohols/AdminAlcoholsControllerDocsTest.kt b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminAlcoholsControllerDocsTest.kt index 9d49ef6c0..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,7 +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.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 @@ -12,6 +12,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 @@ -21,8 +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.queryParameters +import org.springframework.restdocs.request.RequestDocumentation.* import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.assertj.MockMvcTester @@ -117,4 +117,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..410ee27a1 --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminDistilleryControllerDocsTest.kt @@ -0,0 +1,94 @@ +package app.docs.alcohols + +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.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.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 + +@WebMvcTest( + controllers = [AdminDistilleryController::class], + excludeAutoConfiguration = [SecurityAutoConfiguration::class] +) +@AutoConfigureRestDocs +@DisplayName("Admin Distillery 컨트롤러 RestDocs 테스트") +class AdminDistilleryControllerDocsTest { + + @Autowired + private lateinit var mvc: MockMvcTester + + @MockitoBean + private lateinit var alcoholReferenceService: AlcoholReferenceService + + @Test + @DisplayName("증류소 목록을 조회할 수 있다") + fun getAllDistilleries() { + // given + val items = AlcoholsHelper.createAdminDistilleryItems(3) + val page = PageImpl(items) + val response = GlobalResponse.fromPage(page) + + given(alcoholReferenceService.findAllDistilleries(any(AdminReferenceSearchRequest::class.java))) + .willReturn(response) + + // when & then + assertThat( + mvc.get().uri("/distilleries?keyword=&page=0&size=20&sortOrder=ASC") + ) + .hasStatusOk() + .apply( + document( + "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("응답 코드"), + 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.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(), + 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..442ff0d0d --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminRegionControllerDocsTest.kt @@ -0,0 +1,95 @@ +package app.docs.alcohols + +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.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.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 + +@WebMvcTest( + controllers = [AdminRegionController::class], + excludeAutoConfiguration = [SecurityAutoConfiguration::class] +) +@AutoConfigureRestDocs +@DisplayName("Admin Region 컨트롤러 RestDocs 테스트") +class AdminRegionControllerDocsTest { + + @Autowired + private lateinit var mvc: MockMvcTester + + @MockitoBean + private lateinit var alcoholReferenceService: AlcoholReferenceService + + @Test + @DisplayName("지역 목록을 조회할 수 있다") + fun getAllRegions() { + // given + val items = AlcoholsHelper.createAdminRegionItems(3) + val page = PageImpl(items) + val response = GlobalResponse.fromPage(page) + + given(alcoholReferenceService.findAllRegionsForAdmin(any(AdminReferenceSearchRequest::class.java))) + .willReturn(response) + + // when & then + assertThat( + mvc.get().uri("/regions?keyword=&page=0&size=20&sortOrder=ASC") + ) + .hasStatusOk() + .apply( + document( + "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("응답 코드"), + 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.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(), + 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..351dbff82 --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/docs/alcohols/AdminTastingTagControllerDocsTest.kt @@ -0,0 +1,95 @@ +package app.docs.alcohols + +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.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.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 + +@WebMvcTest( + controllers = [AdminTastingTagController::class], + excludeAutoConfiguration = [SecurityAutoConfiguration::class] +) +@AutoConfigureRestDocs +@DisplayName("Admin TastingTag 컨트롤러 RestDocs 테스트") +class AdminTastingTagControllerDocsTest { + + @Autowired + private lateinit var mvc: MockMvcTester + + @MockitoBean + private lateinit var alcoholReferenceService: AlcoholReferenceService + + @Test + @DisplayName("테이스팅 태그 목록을 조회할 수 있다") + fun getAllTastingTags() { + // given + val items = AlcoholsHelper.createAdminTastingTagItems(3) + val page = PageImpl(items) + val response = GlobalResponse.fromPage(page) + + given(alcoholReferenceService.findAllTastingTags(any(AdminReferenceSearchRequest::class.java))) + .willReturn(response) + + // when & then + assertThat( + mvc.get().uri("/tasting-tags?keyword=&page=0&size=20&sortOrder=ASC") + ) + .hasStatusOk() + .apply( + document( + "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("응답 코드"), + 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.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(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("API 경로 버전").ignored() + ) + ) + ) + } +} 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 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() } 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..3cd4a670e --- /dev/null +++ b/bottlenote-admin-api/src/test/kotlin/app/integration/alcohols/AdminReferenceDataIntegrationTest.kt @@ -0,0 +1,154 @@ +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?page=0&size=20") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + + // 페이지네이션 메타 정보 확인 + assertThat( + mockMvcTester.get().uri("/tasting-tags?page=0&size=10") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.meta.size").isEqualTo(10) + } + + @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?page=0&size=20") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + } + + @Test + @DisplayName("지역 목록이 페이지네이션 메타 정보를 포함한다") + fun getRegionsWithPaginationMeta() { + // given + alcoholTestFactory.persistAlcohols(1) + + // when & then - 응답 데이터 및 페이지네이션 메타 확인 + val result = mockMvcTester.get().uri("/regions?page=0&size=10") + .header("Authorization", "Bearer $accessToken") + + assertThat(result) + .hasStatusOk() + .bodyJson() + .extractingPath("$.meta.page").isEqualTo(0) + + assertThat(result) + .bodyJson() + .extractingPath("$.meta.totalElements").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?page=0&size=20") + .header("Authorization", "Bearer $accessToken") + ) + .hasStatusOk() + .bodyJson() + .extractingPath("$.success").isEqualTo(true) + } + + @Test + @DisplayName("증류소 목록을 키워드로 검색할 수 있다") + fun getDistilleriesWithKeyword() { + // given + alcoholTestFactory.persistAlcohols(1) + + // when & then - 키워드 검색 + val result = mockMvcTester.get().uri("/distilleries?keyword=&page=0&size=20") + .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..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; @@ -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..e43d2cd38 --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/domain/DistilleryRepository.java @@ -0,0 +1,10 @@ +package app.bottlenote.alcohols.domain; + +import app.bottlenote.alcohols.dto.response.AdminDistilleryItem; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface DistilleryRepository { + + 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 e406586e9..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 @@ -1,9 +1,14 @@ package app.bottlenote.alcohols.domain; +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(); + + 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 fd20bf58e..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 @@ -1,8 +1,13 @@ package app.bottlenote.alcohols.domain; +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(); + + 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..2bd360dfc --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/dto/request/AdminReferenceSearchRequest.java @@ -0,0 +1,29 @@ +package app.bottlenote.alcohols.dto.request; + +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 + * + * @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; + } + + public Pageable toPageable() { + return PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(sortOrder.name()), "id")); + } +} 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..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,6 +8,7 @@ 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; @@ -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..16f4d35eb --- /dev/null +++ b/bottlenote-mono/src/main/java/app/bottlenote/alcohols/repository/JpaDistilleryRepository.java @@ -0,0 +1,30 @@ +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 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 + 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 + where (:keyword is null or :keyword = '' + or d.korName like concat('%', :keyword, '%') + or d.engName like concat('%', :keyword, '%')) + """) + 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 605d57a38..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 @@ -2,11 +2,15 @@ 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; +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 { @@ -14,8 +18,21 @@ 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 + where (:keyword is null or :keyword = '' + or r.korName like concat('%', :keyword, '%') + or r.engName like concat('%', :keyword, '%')) + """) + 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 fbbf74463..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 @@ -2,9 +2,28 @@ 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 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 - 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 + where (:keyword is null or :keyword = '' + or t.korName like concat('%', :keyword, '%') + or t.engName like concat('%', :keyword, '%')) + """) + Page findAllTastingTags(@Param("keyword") String keyword, Pageable pageable); +} 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()); + } } 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/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..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 @@ -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; @@ -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(); + } } 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..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 @@ -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,36 @@ 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)) { 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(); + } } 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