Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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