From e172af738d29fb609c82b002631cfb0cb0ceca5e Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Sat, 28 Feb 2026 17:30:57 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[SCRUM-442]=20feat:=20=EC=A7=80=EC=97=AD=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=84=9C=EC=9A=B8=EC=9D=B8=EC=B2=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../be/store/controller/StoreController.java | 62 +++++++++++-------- .../response/StoreRegionCountResponse.java | 8 +++ .../repository/store/StoreRepository.java | 19 ++++++ .../be/store/service/StoreService.java | 5 ++ 5 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/kkinikong/be/store/dto/response/StoreRegionCountResponse.java diff --git a/build.gradle b/build.gradle index 2b42482..e97d38e 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ dependencies { // mysql runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc' runtimeOnly 'com.mysql:mysql-connector-j:8.0.33' + runtimeOnly 'com.mysql:mysql-connector-j' // test testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/com/kkinikong/be/store/controller/StoreController.java b/src/main/java/com/kkinikong/be/store/controller/StoreController.java index f34f1f0..c367515 100644 --- a/src/main/java/com/kkinikong/be/store/controller/StoreController.java +++ b/src/main/java/com/kkinikong/be/store/controller/StoreController.java @@ -42,16 +42,28 @@ public ResponseEntity> getTopStore( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(response)); } + @Operation( + summary = "메인페이지 가맹점 지역 개수 조회", + description = + """ + - 지역은 가맹점 region에서 추출하며, 서울, 인천 지역 제외한 가맹점 지역 개수를 조회합니다. + """) + @GetMapping("/region-count") + public ResponseEntity> getStoreRegionCount() { + StoreRegionCountResponse response = storeService.getStoreRegionCount(); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(response)); + } + @Operation( summary = "가맹점 찾기 화면 리스트/검색 통합 조회", description = """ - - keyword가 없으면: GPS 또는 디폴트 위치(인천 서구) 기준 전체 가맹점 조회 - - keyword가 주소(00동, 00구 등)일 경우: 해당 위치 기준 반경 5km 가맹점 조회 - - keyword가 음식명 등 일반 키워드일 경우: 현재 위치 기준 가맹점명/주소에 포함된 가맹점 조회 - - category 필터 선택안하면 전체 가맹점 조회 - - 정렬: 거리순(DISTANCE), 별점순(RATING), 리뷰순(REVIEW_COUNT), 조회수순(VIEW_COUNT) - """) + - keyword가 없으면: GPS 또는 디폴트 위치(인천 서구) 기준 전체 가맹점 조회 + - keyword가 주소(00동, 00구 등)일 경우: 해당 위치 기준 반경 5km 가맹점 조회 + - keyword가 음식명 등 일반 키워드일 경우: 현재 위치 기준 가맹점명/주소에 포함된 가맹점 조회 + - category 필터 선택안하면 전체 가맹점 조회 + - 정렬: 거리순(DISTANCE), 별점순(RATING), 리뷰순(REVIEW_COUNT), 조회수순(VIEW_COUNT) + """) @GetMapping("/list") public ResponseEntity> getStoresList( @Parameter(description = "인천 서구의 임의의 위도 값", example = "37.545472") @@ -76,12 +88,12 @@ public ResponseEntity> getStoresList( summary = "가맹점 지도 화면 리스트/검색 통합 조회", description = """ - - keyword가 없으면: GPS 또는 디폴트 위치(인천 서구) 기준 전체 가맹점 조회 - - keyword가 주소(00동, 00구 등)일 경우: 해당 위치 기준 반경 5km 가맹점 조회 - - keyword가 음식명 등 일반 키워드일 경우: 현재 위치 기준 가맹점명/주소에 포함된 가맹점 조회 - - category 필터 선택안하면 전체 가맹점 조회 - - radius를 통해 반경을 조절할 수 있다. - """) + - keyword가 없으면: GPS 또는 디폴트 위치(인천 서구) 기준 전체 가맹점 조회 + - keyword가 주소(00동, 00구 등)일 경우: 해당 위치 기준 반경 5km 가맹점 조회 + - keyword가 음식명 등 일반 키워드일 경우: 현재 위치 기준 가맹점명/주소에 포함된 가맹점 조회 + - category 필터 선택안하면 전체 가맹점 조회 + - radius를 통해 반경을 조절할 수 있다. + """) @GetMapping("/map") public ResponseEntity> getStoreMapList( @Parameter(description = "인천 서구의 임의의 위도 값", example = "37.545472") @@ -107,13 +119,13 @@ public ResponseEntity> getStoreMapList( summary = "가맹점 상세 정보 조회", description = """ - - 가맹점 ID를 통해 가맹점 정보를 조회합니다. - - 가맹점 ID, 가맹점 카테고리, 가맹점 이름, 가맹점 주소, 영업시간, 스크랩 수, 업데이트 일자, 리뷰 수, 별점, 스크랩 여부를 포함합니다. - - 로그인 한 경우에는 스크랩 여부가 true/false로 반환되고 로그인 하지 않은 경우에는 null로 반환됩니다. - - 가맹점 영업시간 정보는 구글 API 호출을 통해 조회하고 캐싱되어 30일간 유지됩니다. - - 영업시간 정보가 없는 경우, 영업시간 리스트에서 휴무일인 경우에 null로 반환됩니다. - - 가맹점이 ID로 조회되지 않는 경우에는 STORE_NOT_FOUND 400 에러를 반환합니다. - """) + - 가맹점 ID를 통해 가맹점 정보를 조회합니다. + - 가맹점 ID, 가맹점 카테고리, 가맹점 이름, 가맹점 주소, 영업시간, 스크랩 수, 업데이트 일자, 리뷰 수, 별점, 스크랩 여부를 포함합니다. + - 로그인 한 경우에는 스크랩 여부가 true/false로 반환되고 로그인 하지 않은 경우에는 null로 반환됩니다. + - 가맹점 영업시간 정보는 구글 API 호출을 통해 조회하고 캐싱되어 30일간 유지됩니다. + - 영업시간 정보가 없는 경우, 영업시간 리스트에서 휴무일인 경우에 null로 반환됩니다. + - 가맹점이 ID로 조회되지 않는 경우에는 STORE_NOT_FOUND 400 에러를 반환합니다. + """) @GetMapping("/{storeId}") public ResponseEntity> getStoreInfo( @PathVariable("storeId") Long storeId, @@ -127,12 +139,12 @@ public ResponseEntity> getStoreInfo( summary = "가맹점 메뉴 보러가기 및 길찾기", description = """ - - 가맹점 ID를 통해 가맹점 메뉴 보러가기 및 길찾기 링크를 조회합니다. - - 카카오 API를 통해 가맹점 ID를 조회하고, 해당 ID를 통해 메뉴 보러가기 및 길찾기 링크를 생성합니다. - - 카카오에 등록된 가맹점이 없는 경우에는 네이버 검색 링크로 대체합니다. - - 저장된 외부링크는 API 호출을 줄이기 위해 캐싱되어 30일간 유지됩니다. - - 가맹점이 ID로 조회되지 않는 경우에는 STORE_NOT_FOUND 400 에러를 반환합니다. - """) + - 가맹점 ID를 통해 가맹점 메뉴 보러가기 및 길찾기 링크를 조회합니다. + - 카카오 API를 통해 가맹점 ID를 조회하고, 해당 ID를 통해 메뉴 보러가기 및 길찾기 링크를 생성합니다. + - 카카오에 등록된 가맹점이 없는 경우에는 네이버 검색 링크로 대체합니다. + - 저장된 외부링크는 API 호출을 줄이기 위해 캐싱되어 30일간 유지됩니다. + - 가맹점이 ID로 조회되지 않는 경우에는 STORE_NOT_FOUND 400 에러를 반환합니다. + """) @GetMapping("/{storeId}/external-links") public ResponseEntity> getStoreExternalLink( @PathVariable("storeId") Long storeId) { diff --git a/src/main/java/com/kkinikong/be/store/dto/response/StoreRegionCountResponse.java b/src/main/java/com/kkinikong/be/store/dto/response/StoreRegionCountResponse.java new file mode 100644 index 0000000..dcb486c --- /dev/null +++ b/src/main/java/com/kkinikong/be/store/dto/response/StoreRegionCountResponse.java @@ -0,0 +1,8 @@ +package com.kkinikong.be.store.dto.response; + +public record StoreRegionCountResponse(Long count) { + + public static StoreRegionCountResponse from(Long count) { + return new StoreRegionCountResponse(count); + } +} diff --git a/src/main/java/com/kkinikong/be/store/repository/store/StoreRepository.java b/src/main/java/com/kkinikong/be/store/repository/store/StoreRepository.java index d568185..7d8022e 100644 --- a/src/main/java/com/kkinikong/be/store/repository/store/StoreRepository.java +++ b/src/main/java/com/kkinikong/be/store/repository/store/StoreRepository.java @@ -24,4 +24,23 @@ public interface StoreRepository extends JpaRepository, StoreReposi @Modifying(clearAutomatically = true) @Query("UPDATE Store s SET s.viewCount = s.viewCount + :count WHERE s.id = :storeId") void incrementViews(@Param("storeId") Long storeId, @Param("count") Long count); + + @Query( + value = + """ + SELECT COUNT(DISTINCT city_name) AS city_count + FROM ( + SELECT + CASE + WHEN region LIKE '서울%' OR region LIKE '인천%' THEN NULL + WHEN region LIKE '%광역시%' THEN SUBSTRING_INDEX(TRIM(region), ' ', 1) + WHEN region LIKE '%시' THEN SUBSTRING_INDEX(TRIM(region), ' ', -1) + ELSE NULL + END AS city_name + FROM stores + ) t + WHERE city_name IS NOT NULL + """, + nativeQuery = true) + long getStoreRegionCount(); } diff --git a/src/main/java/com/kkinikong/be/store/service/StoreService.java b/src/main/java/com/kkinikong/be/store/service/StoreService.java index 749638a..d0d6e48 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreService.java @@ -164,6 +164,11 @@ public List getTopViewedStores(Double latitude, Double longit return stores.stream().map(StoreCardResponse::from).toList(); } + public StoreRegionCountResponse getStoreRegionCount() { + Long regionCounts = storeRepository.getStoreRegionCount(); + return StoreRegionCountResponse.from(regionCounts); + } + @Transactional public StoreScrapResponse addScrap(Long storeId, Long userId) { User user = getUserOrThrow(userId); From a8431245a07fca36676b0699ec6bb71c137ece87 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Sat, 28 Feb 2026 17:45:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[SCRUM-442]=20fix:=20=EA=B5=AC=EA=B8=80=20a?= =?UTF-8?q?pi=20=ED=8C=8C=EC=8B=B1=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/google/StoreGoogleApiClient.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/kkinikong/be/store/util/google/StoreGoogleApiClient.java b/src/main/java/com/kkinikong/be/store/util/google/StoreGoogleApiClient.java index 5f58bbc..0769d7e 100644 --- a/src/main/java/com/kkinikong/be/store/util/google/StoreGoogleApiClient.java +++ b/src/main/java/com/kkinikong/be/store/util/google/StoreGoogleApiClient.java @@ -121,21 +121,27 @@ private Map> parseOpeningHours(List openingHours) { private String convertTo24Hour(String time) { try { - boolean isAm = time.contains("오전"); - boolean isPm = time.contains("오후"); + time = time.trim(); - time = time.replace("오전", "").replace("오후", "").trim(); + boolean isAm = time.contains("오전") || time.toUpperCase().contains("AM"); + boolean isPm = time.contains("오후") || time.toUpperCase().contains("PM"); + + // 토큰 제거 (오전/오후/AM/PM) + time = + time.replace("오전", "") + .replace("오후", "") + .replace("AM", "") + .replace("PM", "") + .replace("am", "") + .replace("pm", "") + .trim(); String[] timeParts = time.split(":"); int hour = Integer.parseInt(timeParts[0].trim()); int minute = Integer.parseInt(timeParts[1].trim()); - if (isPm && hour != 12) { - hour += 12; - } - if (isAm && hour == 12) { - hour = 0; - } + if (isPm && hour != 12) hour += 12; + if (isAm && hour == 12) hour = 0; return String.format("%02d:%02d", hour, minute); } catch (Exception e) {