Skip to content

Commit f584707

Browse files
authored
Merge pull request #134 from RunChuck/SCRUM-241-국문관광정보-API-동기화-구현
[SCRUM-241] 국문관광정보 API 동기화 구현 (#104)
2 parents 882b13e + d5663d1 commit f584707

12 files changed

Lines changed: 726 additions & 68 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.server.running_handai.domain.course.dto;
2+
3+
public record CourseTrackPointDto(
4+
long courseId,
5+
double startPointLat,
6+
double startPointLon,
7+
double endPointLat,
8+
double endPointLon
9+
) {
10+
}

src/main/java/com/server/running_handai/domain/course/repository/TrackPointRepository.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.server.running_handai.domain.course.repository;
22

3+
import com.server.running_handai.domain.course.dto.CourseTrackPointDto;
34
import com.server.running_handai.domain.course.entity.Course;
45
import com.server.running_handai.domain.course.entity.TrackPoint;
56
import java.util.List;
67
import java.util.Optional;
78
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
810

911
public interface TrackPointRepository extends JpaRepository<TrackPoint, Long> {
1012

@@ -32,4 +34,23 @@ public interface TrackPointRepository extends JpaRepository<TrackPoint, Long> {
3234
* 코스 ID 목록으로 모든 트랙포인트를 한 번에 조회
3335
*/
3436
List<TrackPoint> findByCourseIdInOrderBySequenceAsc(List<Long> courseIds);
37+
38+
/**
39+
* DB에 저장된 모든 코스별 시작점, 도착점 조회
40+
*/
41+
@Query(
42+
value = "SELECT " +
43+
" c.course_id AS courseId, " +
44+
" stp.lat AS startPointLat, " +
45+
" stp.lon AS startPointLon, " +
46+
" etp.lat AS endPointLat, " +
47+
" etp.lon AS endPointLon " +
48+
"FROM course c " +
49+
"JOIN track_point stp ON c.course_id = stp.course_id AND stp.sequence = 1 " +
50+
"JOIN track_point etp ON c.course_id = etp.course_id AND etp.sequence = (" +
51+
"SELECT MAX(mtp.sequence) FROM track_point mtp WHERE mtp.course_id = c.course_id" +
52+
")",
53+
nativeQuery = true
54+
)
55+
List<CourseTrackPointDto> findAllCourseTrackPoint();
3556
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.server.running_handai.domain.spot.client;
2+
3+
import com.server.running_handai.domain.spot.dto.SpotSyncApiResponseDto;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.reactive.function.client.WebClient;
9+
import org.springframework.web.util.UriComponentsBuilder;
10+
11+
import java.net.URI;
12+
import java.time.LocalDate;
13+
import java.time.format.DateTimeFormatter;
14+
15+
@Slf4j
16+
@Component
17+
@RequiredArgsConstructor
18+
public class SpotSyncApiClient {
19+
private final WebClient webClient;
20+
21+
@Value("${external.api.spot.base-url}")
22+
private String baseUrl;
23+
24+
@Value("${external.api.spot.service-key}")
25+
private String serviceKey;
26+
27+
/**
28+
* [국문 관광정보] 관광정보 동기화 목록 조회 API를 요청합니다.
29+
* 요청한 수정일을 기준으로 해당 날짜에 변경된 콘텐츠가 있는 경우 해당 콘텐츠의 정보를 응답합니다.
30+
*
31+
* @param areaCode 지역 코드
32+
* @return SpotSyncApiResponseDto
33+
*/
34+
public SpotSyncApiResponseDto fetchSpotSyncData(int areaCode, String date) {
35+
// URL 생성
36+
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl)
37+
.path("/areaBasedSyncList2")
38+
.queryParam("MobileOS", "ETC")
39+
.queryParam("MobileApp", "runninghandai")
40+
.queryParam("_type", "json")
41+
.queryParam("areaCode", String.valueOf(areaCode))
42+
.queryParam("modifiedtime", date)
43+
.queryParam("serviceKey", serviceKey);
44+
45+
URI uri = builder.build(true).toUri();
46+
47+
// API 호출
48+
return webClient.get()
49+
.uri(uri)
50+
.retrieve()
51+
.bodyToMono(SpotSyncApiResponseDto.class)
52+
.block();
53+
}
54+
}

src/main/java/com/server/running_handai/domain/spot/controller/SpotDataController.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import com.server.running_handai.global.response.ResponseCode;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.http.ResponseEntity;
8-
import org.springframework.web.bind.annotation.PathVariable;
9-
import org.springframework.web.bind.annotation.PutMapping;
10-
import org.springframework.web.bind.annotation.RequestMapping;
11-
import org.springframework.web.bind.annotation.RestController;
8+
import org.springframework.web.bind.annotation.*;
9+
10+
import java.time.LocalDate;
11+
import java.time.format.DateTimeFormatter;
1212

1313
@RestController
1414
@RequestMapping("/api/admin/courses")
@@ -21,4 +21,23 @@ public ResponseEntity<CommonResponse<?>> updateSpots(@PathVariable Long courseId
2121
spotDataService.updateSpots(courseId);
2222
return ResponseEntity.ok().body(CommonResponse.success(ResponseCode.SUCCESS, null));
2323
}
24+
25+
@PostMapping("/sync-spots/date")
26+
public ResponseEntity<CommonResponse<?>> syncSpotsByDate(@RequestParam(required = false) String date) {
27+
String targetDate = date;
28+
if (targetDate == null) {
29+
// 호출 기준 전날 날짜 계산 (YYYYMMDD)
30+
targetDate = LocalDate.now()
31+
.minusDays(1)
32+
.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
33+
}
34+
spotDataService.syncSpotsByDate(targetDate);
35+
return ResponseEntity.ok().body(CommonResponse.success(ResponseCode.SUCCESS, null));
36+
}
37+
38+
@PostMapping("/sync-spots/location")
39+
public ResponseEntity<CommonResponse<?>> syncSpotsByLocation() {
40+
spotDataService.syncSpotsByLocation();
41+
return ResponseEntity.ok().body(CommonResponse.success(ResponseCode.SUCCESS, null));
42+
}
2443
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.server.running_handai.domain.spot.dto;
2+
3+
import java.util.Arrays;
4+
import java.util.Collections;
5+
import java.util.Set;
6+
import java.util.stream.Collectors;
7+
8+
public record SpotExternalIdsDto(
9+
Long courseId,
10+
String externalIds
11+
) {
12+
public Set<String> getSpotExternalIds() {
13+
if (externalIds == null || externalIds.isEmpty()) {
14+
return Collections.emptySet();
15+
}
16+
17+
return Arrays.stream(externalIds.split(","))
18+
.map(String::trim)
19+
.filter(externalId -> !externalId.isEmpty())
20+
.collect(Collectors.toSet());
21+
}
22+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.server.running_handai.domain.spot.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import io.swagger.v3.oas.annotations.Hidden;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import lombok.ToString;
9+
10+
import java.util.List;
11+
12+
@Hidden
13+
@Getter
14+
@ToString
15+
@NoArgsConstructor
16+
public class SpotSyncApiResponseDto {
17+
@JsonProperty("response")
18+
private SpotSyncApiResponseDto.Response response;
19+
20+
@Getter
21+
@ToString
22+
@NoArgsConstructor
23+
public static class Response {
24+
@JsonProperty("header")
25+
private SpotSyncApiResponseDto.Header header;
26+
27+
@JsonProperty("body")
28+
private SpotSyncApiResponseDto.Body body;
29+
}
30+
31+
@Getter
32+
@ToString
33+
@NoArgsConstructor
34+
public static class Header {
35+
@JsonProperty("resultCode")
36+
private String resultCode;
37+
38+
@JsonProperty("resultMsg")
39+
private String resultMsg;
40+
}
41+
42+
@Getter
43+
@ToString
44+
@NoArgsConstructor
45+
public static class Body {
46+
@JsonProperty("items")
47+
private SpotSyncApiResponseDto.Items items;
48+
49+
@JsonProperty("numOfRows")
50+
private int numOfRows;
51+
52+
@JsonProperty("pageNo")
53+
private int pageNo;
54+
55+
@JsonProperty("totalCount")
56+
private int totalCount;
57+
}
58+
59+
@Getter
60+
@ToString
61+
@NoArgsConstructor
62+
public static class Items {
63+
@JsonProperty("item")
64+
private List<SpotSyncApiResponseDto.Item> itemList;
65+
}
66+
67+
/**
68+
* [국문 관광정보] 관광정보 동기화 목록 조회
69+
*/
70+
@Getter
71+
@ToString
72+
@NoArgsConstructor
73+
@JsonIgnoreProperties(ignoreUnknown = true)
74+
public static class Item {
75+
76+
@JsonProperty("contentid")
77+
private String spotExternalId; // 장소 고유번호 (Spot.externalId)
78+
79+
@JsonProperty("showflag")
80+
private String spotShowflag; // 콘텐츠 표출여부 (1: 표출, 0: 비표출)
81+
}
82+
}

src/main/java/com/server/running_handai/domain/spot/entity/Spot.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ public Spot(String externalId, String name, String address, String description,
6363
this.lon = lon;
6464
}
6565

66+
/**
67+
* API 데이터와 비교하여 Spot 엔티티의 필드를 업데이트합니다.
68+
* 변경이 발생했을 경우에만 true를 반환합니다.
69+
*
70+
* @param source 비교 대상이 되는, API 응답으로부터 변환된 Spot 객체
71+
* @return 내용 변경이 있었는지 여부
72+
*/
73+
public boolean syncWith(Spot source) {
74+
boolean isUpdated = false;
75+
76+
if (!this.name.equals(source.getName())) {
77+
this.name = source.getName();
78+
isUpdated = true;
79+
}
80+
if (!this.address.equals(source.getAddress())) {
81+
this.address = source.getAddress();
82+
isUpdated = true;
83+
}
84+
if (!this.description.equals(source.getDescription())) {
85+
this.description = source.getDescription();
86+
isUpdated = true;
87+
}
88+
if (this.spotCategory != source.getSpotCategory()) {
89+
this.spotCategory = source.getSpotCategory();
90+
isUpdated = true;
91+
}
92+
if (this.lat != source.getLat()) {
93+
this.lat = source.getLat();
94+
isUpdated = true;
95+
}
96+
if (this.lon != source.getLon()) {
97+
this.lon = source.getLon();
98+
isUpdated = true;
99+
}
100+
101+
return isUpdated;
102+
}
103+
66104
// ==== 연관관계 편의 메서드 ==== //
67105
public void setSpotImage(SpotImage spotImage) {
68106
this.spotImage = spotImage;

src/main/java/com/server/running_handai/domain/spot/repository/CourseSpotRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,20 @@
66
import org.springframework.data.jpa.repository.Query;
77
import org.springframework.data.repository.query.Param;
88

9+
import java.util.Set;
10+
911
public interface CourseSpotRepository extends JpaRepository<CourseSpot, Long> {
12+
/**
13+
* 특정 코스에 연결된 모든 장소의 연관관계를 모두 삭제합니다.
14+
*/
1015
@Modifying
1116
@Query("DELETE FROM CourseSpot cs WHERE cs.course.id = :courseId")
1217
void deleteByCourseId(@Param("courseId") Long courseId);
18+
19+
/**
20+
* 특정 코스에 연결된 장소 중 External Id의 목록에 포함된 장소의 연관관계만 삭제합니다.
21+
*/
22+
@Modifying
23+
@Query("DELETE FROM CourseSpot cs WHERE cs.course.id = :courseId AND cs.spot.externalId IN :externalIds")
24+
void deleteByCourseIdAndSpotExternalIdIn(@Param("courseId") Long courseId, @Param("externalIds") Set<String> externalIds);
1325
}

0 commit comments

Comments
 (0)