Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ dependencies {
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

// QueryDSL
Expand All @@ -57,6 +59,10 @@ dependencies {
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Spring AI
implementation 'org.springframework.ai:spring-ai-core:1.0.0-M6'
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/ssafy/trabuddy/common/config/AiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ssafy.trabuddy.common.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

@Bean
ChatClient simpleChatClient(ChatClient.Builder builder) {
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
import com.ssafy.trabuddy.domain.attraction.model.dto.AttractionSearchResponse;
import com.ssafy.trabuddy.domain.attraction.model.dto.GetAttractionResponse;
import com.ssafy.trabuddy.domain.attraction.model.entity.AttractionEntity;
import com.ssafy.trabuddy.domain.attraction.model.enums.AttractionSource;
import com.ssafy.trabuddy.domain.kakaoPlaceSearch.model.dto.KakaoPlaceSearchResponse;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;

import java.time.LocalDateTime;
import java.util.List;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", imports = {LocalDateTime.class, AttractionSource.class})
public interface AttractionMapper {
AttractionMapper INSTANCE = Mappers.getMapper(AttractionMapper.class);

Expand All @@ -22,8 +26,46 @@ public interface AttractionMapper {

List<AttractionSearchResponse> toAttractionSearchResponseList(List<AttractionEntity> attractionEntities);

// 카카오 API에서 제공하지 않는 값은 빈 string이나 null 등 더미 값으로 설정
@Mapping(source = "id", target = "contentId")
@Mapping(source = "placeName", target = "title")
@Mapping(source = "phone", target = "telephone", qualifiedByName = "nullSafeString")
@Mapping(source = "addressName", target = "address1", qualifiedByName = "nullSafeString")
@Mapping(source = "roadAddressName", target = "address2", qualifiedByName = "nullSafeString")
@Mapping(source = "y", target = "latitude", qualifiedByName = "stringToDouble")
@Mapping(source = "x", target = "longitude", qualifiedByName = "stringToDouble")
@Mapping(target = "attractionId", ignore = true) // 자동 생성
@Mapping(target = "contentTypeId", constant = "")
@Mapping(target = "createdTime", expression = "java(LocalDateTime.now())")
@Mapping(target = "modifiedTime", expression = "java(LocalDateTime.now())")
@Mapping(target = "zipCode", constant = "")
@Mapping(target = "category1", constant = "")
@Mapping(target = "category2", constant = "")
@Mapping(target = "category3", constant = "")
@Mapping(target = "mapLevel", constant = "-1")
@Mapping(target = "firstImageUrl", constant = "")
@Mapping(target = "firstImageThumbnailUrl", constant = "")
@Mapping(target = "copyrightDivisionCode", constant = "")
@Mapping(target = "booktourInfo", constant = "")
@Mapping(target = "source", expression = "java(AttractionSource.kakao)")
@Mapping(target = "sigungu", ignore = true)
@Mapping(target = "area", ignore = true)
AttractionEntity toAttractionEntity(KakaoPlaceSearchResponse.Document document);

default Page<AttractionSearchResponse> toAttractionSearchResponsePage(Page<AttractionEntity> attractionPage) {
List<AttractionSearchResponse> responses = toAttractionSearchResponseList(attractionPage.getContent());
return new PageImpl<>(responses, attractionPage.getPageable(), attractionPage.getTotalElements());
}

// latitude와 longitude 변환을 위한 커스텀 메서드
@Named("stringToDouble")
default double stringToDouble(String value) {
return value != null ? Double.parseDouble(value) : 0.0;
}

// telephone 필드 null 안전 처리
@Named("nullSafeString")
default String nullSafeString(String value) {
return value != null ? value : "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.ssafy.trabuddy.domain.sigungu.model.entity.SigunguEntity;
import lombok.*;

import com.ssafy.trabuddy.domain.attraction.model.enums.AttractionSource;
import java.time.LocalDateTime;

@Getter
Expand Down Expand Up @@ -34,6 +35,8 @@ public class AttractionSearchResponse {
private String copyrightDivisionCode;
private String booktourInfo;

private AttractionSource source;

private SigunguEntity sigungu;

private AreaEntity area;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
import com.ssafy.trabuddy.domain.sigungu.model.entity.SigunguEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import com.ssafy.trabuddy.domain.attraction.model.enums.AttractionSource;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "attraction")
public class AttractionEntity {
@Id
Expand All @@ -39,6 +42,9 @@ public class AttractionEntity {
private String copyrightDivisionCode;
private String booktourInfo;

@Enumerated(EnumType.STRING)
private AttractionSource source;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sigungu_code")
private SigunguEntity sigungu;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ssafy.trabuddy.domain.attraction.model.enums;

public enum AttractionSource {
kakao,
gov
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.ssafy.trabuddy.domain.attraction.repository;

import com.ssafy.trabuddy.domain.attraction.model.entity.AttractionEntity;
import com.ssafy.trabuddy.domain.attraction.model.enums.AttractionSource;
import com.ssafy.trabuddy.domain.attraction.repository.query.AttractionRepositoryCustom;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AttractionRepository extends JpaRepository<AttractionEntity, Long>, AttractionRepositoryCustom {
import java.util.Optional;

public interface AttractionRepository extends JpaRepository<AttractionEntity, Long>, AttractionRepositoryCustom {
Optional<AttractionEntity> findByContentIdAndSource(String contentId, AttractionSource source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.ssafy.trabuddy.domain.kakaoPlaceSearch.controller;

import com.ssafy.trabuddy.domain.kakaoPlaceSearch.model.dto.KakaoPlaceSearchRequest;
import com.ssafy.trabuddy.domain.kakaoPlaceSearch.model.dto.KakaoPlaceSearchResponse;
import com.ssafy.trabuddy.domain.kakaoPlaceSearch.service.KakaoPlaceSearchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
//@RequestMapping("/kakao-place-search")
@RequiredArgsConstructor
@Slf4j
public class KakaoPlaceSearchController {

private final KakaoPlaceSearchService kakaoPlaceSearchService;

/**
* 키워드로 장소를 검색합니다.
*
* @param query 검색 키워드
* @param categoryGroupCode 카테고리 그룹 코드
* @param x 중심 좌표의 X값 (경도)
* @param y 중심 좌표의 Y값 (위도)
* @param radius 검색 반경 (미터)
* @param rect 사각형 범위
* @param page 결과 페이지 번호
* @param size 한 페이지에 보여질 문서 수
* @param sort 정렬 옵션 (distance 또는 accuracy)
* @return 검색 결과
*/
// @GetMapping("/keyword")
public ResponseEntity<KakaoPlaceSearchResponse> searchPlacesByKeyword(
@RequestParam String query,
@RequestParam(required = false) String categoryGroupCode,
@RequestParam(required = false) String x,
@RequestParam(required = false) String y,
@RequestParam(required = false) Integer radius,
@RequestParam(required = false) String rect,
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) String sort
) {
// log.info("키워드 검색 요청: query={}, x={}, y={}, radius={}", query, x, y, radius);

KakaoPlaceSearchRequest request = KakaoPlaceSearchRequest.builder()
.query(query)
.categoryGroupCode(categoryGroupCode)
.x(x)
.y(y)
.radius(radius)
.rect(rect)
.page(page)
.size(size)
.sort(sort)
.build();

KakaoPlaceSearchResponse response = kakaoPlaceSearchService.searchPlacesByKeyword(request);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ssafy.trabuddy.domain.kakaoPlaceSearch.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KakaoPlaceSearchRequest {
private String query; // 검색을 원하는 질의어
private String categoryGroupCode; // 카테고리 그룹 코드
private String x; // 중심 좌표의 X값 (경도)
private String y; // 중심 좌표의 Y값 (위도)
private Integer radius; // 반경 (미터)
private String rect; // 사각형 범위
private Integer page; // 결과 페이지 번호
private Integer size; // 한 페이지에 보여질 문서 수
private String sort; // 정렬 옵션 (distance 또는 accuracy)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.ssafy.trabuddy.domain.kakaoPlaceSearch.model.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class KakaoPlaceSearchResponse {
private Meta meta;
private List<Document> documents;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public static class Meta {
@JsonProperty("total_count")
private int totalCount;

@JsonProperty("pageable_count")
private int pageableCount;

@JsonProperty("is_end")
private boolean isEnd;

@JsonProperty("same_name")
private SameName sameName;
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public static class SameName {
private List<String> region;
private String keyword;

@JsonProperty("selected_region")
private String selectedRegion;
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public static class Document {
@JsonProperty("place_name")
private String placeName;

private String distance;

@JsonProperty("place_url")
private String placeUrl;

@JsonProperty("category_name")
private String categoryName;

@JsonProperty("address_name")
private String addressName;

@JsonProperty("road_address_name")
private String roadAddressName;

private String id;

private String phone;

@JsonProperty("category_group_code")
private String categoryGroupCode;

@JsonProperty("category_group_name")
private String categoryGroupName;

private String x; // longitude
private String y; // latitude
}
}
Loading