Skip to content

Commit f509a0d

Browse files
authored
feat : recommend (#25)
* fix : 관광지 정보 가져오는 것 수정 #21 * feat : 포토스팟 가져오는 기능 추가 #21 * fix : 로그 제거 #21 * feat : 경로 Recommed 부분 추가 #21
1 parent aa3b884 commit f509a0d

File tree

11 files changed

+365
-45
lines changed

11 files changed

+365
-45
lines changed

.github/workflows/deploy.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
echo "spring.datasource.password=${{ secrets.DB_PASSWORD }}" >> src/main/resources/application.properties
2323
echo "spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver" >> src/main/resources/application.properties
2424
25+
echo "spring.data.redis.host=redis >> src/main/resources/application.properties
26+
echo "spring.data.redis.port=6379 >> src/main/resources/application.properties
27+
2528
echo "spring.jpa.hibernate.ddl-auto=update" >> src/main/resources/application.properties
2629
echo "spring.jpa.show-sql=true" >> src/main/resources/application.properties
2730
echo "spring.jpa.properties.hibernate.format_sql=true" >> src/main/resources/application.properties
@@ -37,6 +40,9 @@ jobs:
3740
echo "google.client-id=${{ secrets.GOOGLE_CLIENT_ID }}" >> src/main/resources/application.properties
3841
3942
echo "gpt.key=${{ secrets.GPT_KEY }}" >> src/main/resources/application.properties
43+
echo "tour.end=${{ secrets.TOUR_END }}" >> src/main/resources/application.properties
44+
echo "tour.key=${{ secrets.TOUR_KEY }}" >> src/main/resources/application.properties
45+
4046
4147
- name: ☕ Gradle Build (jar 생성)
4248
run: ./gradlew build -x test
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,58 @@
11
package backend.greatjourney.config;
22

33
// CacheConfig.java
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
45
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
7+
58
import org.springframework.cache.CacheManager;
69
import org.springframework.cache.annotation.EnableCaching;
710
import org.springframework.context.annotation.Bean;
811
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.context.annotation.Primary;
913
import org.springframework.data.redis.cache.RedisCacheConfiguration;
1014
import org.springframework.data.redis.cache.RedisCacheManager;
1115
import org.springframework.data.redis.connection.RedisConnectionFactory;
1216
import org.springframework.data.redis.serializer.*;
1317

1418
import java.time.Duration;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import backend.greatjourney.global.gpt.dto.GptRankResponse;
1523

1624
@Configuration
1725
@EnableCaching
1826
public class CacheConfig {
1927

2028
@Bean
29+
@Primary
2130
public CacheManager cacheManager(RedisConnectionFactory cf, ObjectMapper om) {
22-
RedisSerializationContext.SerializationPair<Object> valueSerializer =
23-
RedisSerializationContext.SerializationPair
24-
.fromSerializer(new GenericJackson2JsonRedisSerializer(om));
2531

26-
RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
32+
// 기본 캐시: 기존 설정 유지(6h, GenericJackson2JsonRedisSerializer 사용)
33+
var defaultValueSer = RedisSerializationContext.SerializationPair
34+
.fromSerializer(new GenericJackson2JsonRedisSerializer(om));
35+
36+
var base = RedisCacheConfiguration.defaultCacheConfig()
2737
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
28-
.serializeValuesWith(valueSerializer)
38+
.serializeValuesWith(defaultValueSer)
2939
.disableCachingNullValues()
3040
.entryTtl(Duration.ofHours(6));
3141

42+
// gptBikeTrails 전용: 구체 타입 직렬화기 + TTL 없음(요청 있을 때만 갱신)
43+
var gptSer = new Jackson2JsonRedisSerializer<>(GptRankResponse.class); // 또는 reader/writer 생성자
44+
var gptCfg = base
45+
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(gptSer))
46+
.entryTtl(Duration.ZERO);
47+
48+
Map<String, RedisCacheConfiguration> perCache = new HashMap<>();
49+
perCache.put("relatedPlaces", base.entryTtl(Duration.ofHours(6)));
50+
perCache.put("gptBikeTrails", gptCfg);
51+
3252
return RedisCacheManager.builder(cf)
3353
.cacheDefaults(base)
34-
.withCacheConfiguration("relatedPlaces", base.entryTtl(Duration.ofHours(6)))
54+
.withInitialCacheConfigurations(perCache)
3555
.build();
3656
}
57+
3758
}

greatjourney/src/main/java/backend/greatjourney/config/WebClientConfig.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.web.reactive.function.client.WebClient;
6+
import org.springframework.web.util.DefaultUriBuilderFactory;
67

78
@Configuration
89
public class WebClientConfig {
910

1011
@Bean
1112
public WebClient tourClient(WebClient.Builder builder){
12-
return builder.baseUrl("https://apis.data.go.kr")
13+
DefaultUriBuilderFactory f =
14+
new DefaultUriBuilderFactory("https://apis.data.go.kr"); // ★ 도메인
15+
f.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); // ★ 추가 인코딩 금지
16+
return WebClient.builder()
17+
.uriBuilderFactory(f)
1318
.build();
1419
}
1520
}

greatjourney/src/main/java/backend/greatjourney/config/client/RegionApiClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public RegionApiResponse callAreaBasedList(String baseYm, String areaCd, String
2929
.queryParam("pageNo", pageNo)
3030
.queryParam("numOfRows", numOfRows)
3131
.queryParam("MobileOS", "AND")
32-
.queryParam("MobileApp", "어디로")
32+
.queryParam("MobileApp", "eodiro")
3333
.queryParam("baseYm", baseYm)
3434
.queryParam("areaCd", areaCd)
3535
.queryParam("signguCd", signguCd)

greatjourney/src/main/java/backend/greatjourney/domain/region/controller/RegionController.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
@RestController
1919
@RequestMapping("/api/v1/region")
2020
@RequiredArgsConstructor
21-
@Tag(name = "지역 근처의 음식점, 숙소, 관광지를 가져오는 API입니다.")
21+
@Tag(name = "지역 근처의 음식점, 숙소, 관광지, 포토스팟을 가져오는 API입니다.")
2222
@Slf4j
2323
public class RegionController {
2424

@@ -53,7 +53,7 @@ public BaseResponse<List<RelatedPlaceDto>> getSleep(@RequestParam String areaNam
5353
@Operation(description = "주변 관광지를 가져오는 API입니다")
5454
public BaseResponse<List<RelatedPlaceDto>> getTour(@RequestParam String areaName, @RequestParam String sigunguName){
5555
//아니면 pageable로 무한 스크롤 가능하게 수정해도 됨
56-
log.info("여기");
56+
5757
return BaseResponse.<List<RelatedPlaceDto>>builder()
5858
.code(200)
5959
.isSuccess(true)
@@ -63,4 +63,17 @@ public BaseResponse<List<RelatedPlaceDto>> getTour(@RequestParam String areaName
6363
}
6464

6565

66+
@GetMapping("/photo")
67+
@Operation(description = "주변 포토스팟를 가져오는 API입니다")
68+
public BaseResponse<List<RelatedPlaceDto>> getPhoto(@RequestParam String areaName, @RequestParam String sigunguName){
69+
//아니면 pageable로 무한 스크롤 가능하게 수정해도 됨
70+
71+
return BaseResponse.<List<RelatedPlaceDto>>builder()
72+
.code(200)
73+
.isSuccess(true)
74+
.message("주변 포토스팟 정보를 가져왔습니다.")
75+
.data(regionService.getRegions(areaName,sigunguName,"photo"))
76+
.build();
77+
}
78+
6679
}

greatjourney/src/main/java/backend/greatjourney/domain/region/entity/PlaceCategory.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,47 @@
44
import lombok.Getter;
55
import java.util.Map;
66

7+
78
@Getter
89
public enum PlaceCategory {
9-
FOOD("음식"),
10-
TOUR("관광지"),
11-
SLEEP("숙박");
12-
private final String kor;
10+
FOOD("음식", null), // lcls = 음식
11+
TOUR("관광지", null), // lcls = 관광지
12+
SLEEP("숙박", null), // lcls = 숙박
13+
PHOTO("관광지", "문화관광"); // lcls = 관광지 AND mcls = 문화관광 ← 신규
14+
15+
private final String lclsKor; // 대분류명(lcls)
16+
private final String mclsKor; // (옵션) 중분류명(mcls)
1317

14-
PlaceCategory(String kor) { this.kor = kor; }
18+
PlaceCategory(String lclsKor, String mclsKor) {
19+
this.lclsKor = lclsKor;
20+
this.mclsKor = mclsKor;
21+
}
1522

23+
// slug 매핑 (원하는 별칭 더 추가 가능)
1624
private static final Map<String, PlaceCategory> BY_SLUG = Map.of(
17-
"food", FOOD,
18-
"tour", TOUR,
19-
"sleep", SLEEP
25+
"food", FOOD,
26+
"tour", TOUR,
27+
"sleep", SLEEP,
28+
"photo", PHOTO,
29+
"photospot", PHOTO
2030
);
2131

2232
public static PlaceCategory fromSlug(String slug) {
2333
PlaceCategory pc = BY_SLUG.get(slug.toLowerCase());
2434
if (pc == null) throw new IllegalArgumentException("Unsupported category: " + slug);
2535
return pc;
2636
}
37+
38+
/**
39+
* 해당 카테고리가 아이템과 매칭되는지 여부
40+
* - mclsKor가 없으면 lcls만 비교
41+
* - mclsKor가 있으면 lcls AND mcls 모두 비교
42+
*/
43+
public boolean matches(backend.greatjourney.domain.region.dto.RegionApiResponse.Item i) {
44+
boolean lclsOk = this.lclsKor.equals(i.getRlteCtgryLclsNm());
45+
if (!lclsOk) return false;
46+
if (this.mclsKor == null) return true;
47+
return this.mclsKor.equals(i.getRlteCtgryMclsNm());
48+
}
2749
}
50+

greatjourney/src/main/java/backend/greatjourney/domain/region/service/RelatedPlaceService.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package backend.greatjourney.domain.region.service;
22

3-
// RelatedPlaceService.java
43
import backend.greatjourney.config.client.RegionApiClient;
54
import backend.greatjourney.domain.region.dto.RegionApiResponse;
65
import backend.greatjourney.domain.region.dto.RelatedPlaceDto;
@@ -18,8 +17,11 @@ public class RelatedPlaceService {
1817

1918
private final RegionApiClient regionApiClient;
2019

21-
@Cacheable(cacheNames = "relatedPlaces",
22-
key = "T(String).format('%s:%s:%s:%s:%s:%s', #baseYm, #areaCd, #signguCd, #pageNo, #numOfRows, #category.kor)")
20+
@Cacheable(
21+
cacheNames = "relatedPlaces",
22+
// ⚠️ TOUR와 PHOTO는 lcls가 모두 '관광지'이므로 kor를 키로 쓰면 충돌 → name() 사용
23+
key = "T(String).format('%s:%s:%s:%s:%s:%s', #baseYm, #areaCd, #signguCd, #pageNo, #numOfRows, #category.name())"
24+
)
2325
public List<RelatedPlaceDto> getRelatedPlacesByCategory(
2426
String baseYm, String areaCd, String signguCd,
2527
int pageNo, int numOfRows, PlaceCategory category
@@ -32,7 +34,7 @@ public List<RelatedPlaceDto> getRelatedPlacesByCategory(
3234
: List.<RegionApiResponse.Item>of();
3335

3436
return items.stream()
35-
.filter(i -> category.getKor().equals(i.getRlteCtgryLclsNm()))
37+
.filter(category::matches) // ← enum에 위임 (PHOTO는 lcls+mcls 동시 필터)
3638
.map(i -> new RelatedPlaceDto(
3739
i.getRlteTatsNm(),
3840
i.getRlteRank(),

greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtTokenProvider.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,8 @@ public Authentication getAuthentication(String token) {
139139
String userId = claims.getSubject();
140140
Long realUserId = Long.parseLong(userId);
141141

142-
log.info(realUserId.toString());
143142
User user = userRepository.findById(realUserId)
144143
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
145-
log.info("dd");
146144
Map<String, Object> attributes = Map.of("userId", userId);
147145
CustomOAuth2User principal = new CustomOAuth2User(attributes, userId, user.getUserRole().name());
148146

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package backend.greatjourney.global.gpt.controller;
22

33
import org.checkerframework.common.util.count.report.qual.ReportUnqualified;
4+
import org.springframework.web.bind.annotation.DeleteMapping;
5+
import org.springframework.web.bind.annotation.GetMapping;
46
import org.springframework.web.bind.annotation.PostMapping;
57
import org.springframework.web.bind.annotation.RequestBody;
68
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RequestParam;
710
import org.springframework.web.bind.annotation.RestController;
811

912
import backend.greatjourney.global.exception.BaseResponse;
13+
import backend.greatjourney.global.gpt.dto.GptRankResponse;
1014
import backend.greatjourney.global.gpt.dto.GptRequest;
1115
import backend.greatjourney.global.gpt.dto.GptResponse;
1216
import backend.greatjourney.global.gpt.service.GptService;
@@ -21,15 +25,34 @@
2125
public class GptController {
2226
private final GptService gptService;
2327

28+
//
29+
// @PostMapping("")
30+
// @Operation(summary = "GPT한테 텍스트로 요청보내는 API입니다.")
31+
// public BaseResponse<GptRankResponse> askGPT(){
32+
// return BaseResponse.<GptRankResponse>builder()
33+
// .code(200)
34+
// .isSuccess(true)
35+
// .message("gpt 답변입니다.")
36+
// .data(gptService.rankBikeTrails())
37+
// .build();
38+
// }
2439

25-
@PostMapping("")
26-
@Operation(summary = "GPT한테 텍스트로 요청보내는 API입니다.")
27-
public BaseResponse<GptResponse> askGPT(@RequestBody GptRequest gptRequest){
28-
return BaseResponse.<GptResponse>builder()
29-
.code(200)
30-
.isSuccess(true)
31-
.message("gpt 답변입니다.")
32-
.data(gptService.askGpt(gptRequest.question()))
33-
.build();
40+
41+
@GetMapping("/bike-trails")
42+
@Operation(summary = "추천 TOP3 자전거길 받는 API입니다.")
43+
public GptRankResponse get() {
44+
return gptService.rankBikeTrails();
45+
}
46+
47+
@PostMapping("/bike-trails/refresh")
48+
@Operation(summary = "추천 TOP3 자전거길 새로고침하는 API입니다.")
49+
public GptRankResponse refresh(@RequestParam Long seed) {
50+
return gptService.refreshBikeTrailsRandom(0.6,seed);
51+
}
52+
53+
@DeleteMapping("/bike-trails/cache")
54+
@Operation(summary = "지금까지 있는 TOP3 자전거기를 삭제하는 API입니다.")
55+
public void evict() {
56+
gptService.evictBikeTrails();
3457
}
3558
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// GptRankResponse.java
2+
package backend.greatjourney.global.gpt.dto;
3+
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
public record GptRankResponse(
8+
Map<String, Double> weights, // 사용한 가중치 그대로 반환
9+
List<TopItem> top3, // 상위 3개
10+
List<String> extra_considerations // 추가 고려사항
11+
) {
12+
public record TopItem(
13+
String name,
14+
double score, // 가중합(= Σ weight * criterion_score)
15+
Reasons reasons, // 원 점수(0~10) + 한 줄 근거
16+
WeightedBreakdown weighted // 각 기준별 가중 기여도
17+
) {}
18+
19+
public record Reasons(
20+
double accessibility, String accessibility_reason,
21+
double scenery, String scenery_reason,
22+
double difficulty, String difficulty_reason,
23+
double infra, String infra_reason,
24+
double season, String season_reason
25+
) {}
26+
27+
public record WeightedBreakdown(
28+
double accessibility, // weight.accessibility * reasons.accessibility
29+
double scenery,
30+
double difficulty,
31+
double infra,
32+
double season
33+
) {}
34+
}

0 commit comments

Comments
 (0)