Skip to content

Commit d2a4df4

Browse files
authored
[Feat] 맛집 가게 조회 API 구현
2 parents 1f375ca + 841179a commit d2a4df4

31 files changed

+636
-29
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package eatda.client.map;
2+
3+
import lombok.Getter;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
6+
@Getter
7+
@ConfigurationProperties(prefix = "kakao")
8+
public class KakaoProperties {
9+
10+
private final String apiKey;
11+
12+
public KakaoProperties(String apiKey) {
13+
validateApiKey(apiKey);
14+
this.apiKey = apiKey;
15+
}
16+
17+
private void validateApiKey(String apiKey) {
18+
if (apiKey == null || apiKey.isBlank()) {
19+
throw new IllegalArgumentException("API key must not be null or blank");
20+
}
21+
}
22+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package eatda.client.map;
2+
3+
import eatda.domain.store.Coordinates;
4+
import java.util.List;
5+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
6+
import org.springframework.http.HttpStatusCode;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.client.RestClient;
9+
10+
@Component
11+
@EnableConfigurationProperties(KakaoProperties.class)
12+
public class MapClient {
13+
14+
private final RestClient restClient;
15+
private final KakaoProperties kakaoProperties;
16+
17+
public MapClient(RestClient.Builder restClient, KakaoProperties properties) {
18+
this.restClient = restClient
19+
.defaultStatusHandler(HttpStatusCode::is5xxServerError, new MapServerErrorHandler())
20+
.build();
21+
this.kakaoProperties = properties;
22+
}
23+
24+
public List<StoreSearchResult> searchShops(String query) {
25+
return restClient.get()
26+
.uri("https://dapi.kakao.com/v2/local/search/keyword.json", builder -> builder
27+
.queryParam("query", query)
28+
.queryParam("category", "FD6")
29+
.queryParam("rect", "%s,%s,%s,%s".formatted(
30+
Coordinates.getMinLongitude(), Coordinates.getMinLatitude(),
31+
Coordinates.getMaxLongitude(), Coordinates.getMaxLatitude()))
32+
.queryParam("page", 1)
33+
.queryParam("size", 15)
34+
.queryParam("sort", "accuracy")
35+
.build())
36+
.header("Authorization", "KakaoAK " + kakaoProperties.getApiKey())
37+
.retrieve()
38+
.body(StoreSearchResults.class)
39+
.results();
40+
}
41+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package eatda.client.map;
2+
3+
import eatda.exception.BusinessErrorCode;
4+
import eatda.exception.BusinessException;
5+
import java.io.IOException;
6+
import org.springframework.http.HttpRequest;
7+
import org.springframework.http.client.ClientHttpResponse;
8+
import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler;
9+
10+
public class MapServerErrorHandler implements ErrorHandler {
11+
12+
@Override
13+
public void handle(HttpRequest request, ClientHttpResponse response) throws IOException {
14+
throw new BusinessException(BusinessErrorCode.MAP_SERVER_ERROR);
15+
}
16+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package eatda.client.map;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
@JsonIgnoreProperties(ignoreUnknown = true)
7+
public record StoreSearchResult(
8+
@JsonProperty("id") String kakaoId,
9+
@JsonProperty("category_group_code") String categoryGroupCode,
10+
@JsonProperty("category_name") String categoryName,
11+
@JsonProperty("phone") String phoneNumber,
12+
@JsonProperty("place_name") String name,
13+
@JsonProperty("place_url") String placeUrl,
14+
@JsonProperty("address_name") String lotNumberAddress,
15+
@JsonProperty("road_address_name") String roadAddress,
16+
@JsonProperty("y") double latitude,
17+
@JsonProperty("x") double longitude
18+
) {
19+
20+
public boolean isFoodStore() {
21+
return "FD6".equals(categoryGroupCode);
22+
}
23+
24+
public boolean isInSeoul() {
25+
if (lotNumberAddress == null || lotNumberAddress.isBlank()) {
26+
return false;
27+
}
28+
return lotNumberAddress.trim().startsWith("서울");
29+
}
30+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package eatda.client.map;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import java.util.List;
6+
7+
@JsonIgnoreProperties(ignoreUnknown = true)
8+
public record StoreSearchResults(@JsonProperty("documents") List<StoreSearchResult> results) {
9+
}

src/main/java/eatda/controller/member/MemberController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package eatda.controller.member;
22

33
import eatda.controller.web.auth.LoginMember;
4-
import eatda.service.service.MemberService;
4+
import eatda.service.member.MemberService;
55
import jakarta.validation.Valid;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.http.ResponseEntity;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package eatda.controller.store;
2+
3+
import eatda.controller.web.auth.LoginMember;
4+
import eatda.service.store.StoreService;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.GetMapping;
8+
import org.springframework.web.bind.annotation.RequestParam;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
@RestController
12+
@RequiredArgsConstructor
13+
public class StoreController {
14+
15+
private final StoreService storeService;
16+
17+
@GetMapping("/api/shop/search")
18+
public ResponseEntity<StoreSearchResponses> searchStore(@RequestParam String query, LoginMember member) {
19+
StoreSearchResponses response = storeService.searchStores(query);
20+
return ResponseEntity.ok(response);
21+
}
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package eatda.controller.store;
2+
3+
import eatda.client.map.StoreSearchResult;
4+
5+
public record StoreSearchResponse(
6+
String kakaoId,
7+
String name,
8+
String address
9+
) {
10+
11+
public StoreSearchResponse(StoreSearchResult searchResult) {
12+
this(
13+
searchResult.kakaoId(),
14+
searchResult.name(),
15+
searchResult.lotNumberAddress()
16+
);
17+
}
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package eatda.controller.store;
2+
3+
import eatda.client.map.StoreSearchResult;
4+
import java.util.List;
5+
6+
public record StoreSearchResponses(List<StoreSearchResponse> stores) {
7+
8+
public static StoreSearchResponses from(List<StoreSearchResult> searchResults) {
9+
List<StoreSearchResponse> storeResponses = searchResults.stream()
10+
.map(StoreSearchResponse::new)
11+
.toList();
12+
return new StoreSearchResponses(storeResponses);
13+
}
14+
}

src/main/java/eatda/domain/store/Coordinates.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ public class Coordinates {
1919
private static final double MAX_LATITUDE = 37.715133;
2020
private static final double MIN_LONGITUDE = 126.734086;
2121
private static final double MAX_LONGITUDE = 127.269311;
22+
23+
public static double getMinLatitude() {
24+
return MIN_LATITUDE;
25+
}
26+
27+
public static double getMaxLatitude() {
28+
return MAX_LATITUDE;
29+
}
30+
31+
public static double getMinLongitude() {
32+
return MIN_LONGITUDE;
33+
}
34+
35+
public static double getMaxLongitude() {
36+
return MAX_LONGITUDE;
37+
}
38+
2239
@Column(nullable = false)
2340
private Double latitude;
2441

0 commit comments

Comments
 (0)