diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/controller/AiplannerController.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/controller/AiplannerController.java new file mode 100644 index 0000000..62d33a4 --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/controller/AiplannerController.java @@ -0,0 +1,39 @@ +package com.swyp.catsgotogedog.aiplanner.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.swyp.catsgotogedog.aiplanner.request.PlannerRequest; +import com.swyp.catsgotogedog.aiplanner.service.AiPlannerService; +import com.swyp.catsgotogedog.global.CatsgotogedogApiResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/planner") +@Slf4j +public class AiplannerController implements AiplannerControllerSwagger { + + private final AiPlannerService aiPlannerService; + + @PostMapping("/initEmbeddingData") + public void initEmbeddingData() { + aiPlannerService.initEmbedContentData(); + } + + @PostMapping("/recommend") + public ResponseEntity> createAiPlanner( + @AuthenticationPrincipal String userId, + @RequestBody PlannerRequest request + ) { + return ResponseEntity.ok( + CatsgotogedogApiResponse.success("플래너 생성 성공", aiPlannerService.createPlan(userId, request)) + ); + } +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/controller/AiplannerControllerSwagger.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/controller/AiplannerControllerSwagger.java new file mode 100644 index 0000000..572777c --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/controller/AiplannerControllerSwagger.java @@ -0,0 +1,45 @@ +package com.swyp.catsgotogedog.aiplanner.controller; + +import org.springframework.http.ResponseEntity; + +import com.swyp.catsgotogedog.aiplanner.request.PlannerRequest; +import com.swyp.catsgotogedog.aiplanner.response.AiplannerResponse; +import com.swyp.catsgotogedog.global.CatsgotogedogApiResponse; +import com.swyp.catsgotogedog.pet.domain.response.PetProfileResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Ai플래너", description = "AI 플래너 관련 API") +public interface AiplannerControllerSwagger { + + @Operation( + summary = "AI 플래너 생성", + description = "AI를 통한 플랜 생성" + ) + @SecurityRequirement(name = "bearer-key") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "플랜 생성 성공", + content = @Content(schema = @Schema(implementation = AiplannerResponse.class))), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자", + content = @Content(schema = @Schema(implementation = CatsgotogedogApiResponse.class))), + @ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음", + content = @Content(schema = @Schema(implementation = CatsgotogedogApiResponse.class))) + }) + ResponseEntity> createAiPlanner( + @Parameter(hidden = true) + String userId, + @RequestBody(description = """ + duration은 DAY_TRIP, ONE_NIGHT, TWO_NIGHT 중 하나의 데이터를 입력해야합니다. + mood는 사용자가 입력한 기분 데이터입니다. + """, required = true) + PlannerRequest request + ); +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/domain/ContentEmbedding.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/domain/ContentEmbedding.java new file mode 100644 index 0000000..64d98f4 --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/domain/ContentEmbedding.java @@ -0,0 +1,39 @@ +package com.swyp.catsgotogedog.aiplanner.domain; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.Mapping; +import org.springframework.data.elasticsearch.annotations.Setting; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Document(indexName = "content-embedding") +@Setting(settingPath = "elasticsearch/content-embed-setting.json") +@Mapping(mappingPath = "elasticsearch/content-embed-mapping.json") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class ContentEmbedding { + + @Id + private Integer contentId; + private int contentTypeId; + private String categoryId; + private int sidoCode; + private int sigunguCode; + private double mapx; + private double mapy; + private String title; + + @Field(type = FieldType.Dense_Vector, dims = 1024) + private List embedding; +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/domain/TravelDuration.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/domain/TravelDuration.java new file mode 100644 index 0000000..be0dfbf --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/domain/TravelDuration.java @@ -0,0 +1,15 @@ +package com.swyp.catsgotogedog.aiplanner.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TravelDuration { + DAY_TRIP(1, "당일치기"), + ONE_NIGHT(2, "1박2일"), + TWO_NIGHT(3, "2박3일"); + + private final int days; + private final String description; +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/repository/ContentEmbeddingRepository.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/repository/ContentEmbeddingRepository.java new file mode 100644 index 0000000..1426848 --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/repository/ContentEmbeddingRepository.java @@ -0,0 +1,11 @@ +package com.swyp.catsgotogedog.aiplanner.repository; + +import java.util.List; + +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import com.swyp.catsgotogedog.aiplanner.domain.ContentEmbedding; + +public interface ContentEmbeddingRepository extends ElasticsearchRepository { + List findByContentIdIn(List contentIds); +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/request/PlannerRequest.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/request/PlannerRequest.java new file mode 100644 index 0000000..189a959 --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/request/PlannerRequest.java @@ -0,0 +1,38 @@ +package com.swyp.catsgotogedog.aiplanner.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.swyp.catsgotogedog.aiplanner.domain.TravelDuration; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Valid +public class PlannerRequest { + + @JsonProperty + @NotNull(message = "여행 기간은 필수입니다.") + private TravelDuration duration; + + @JsonProperty + @NotNull(message = "대분류 여행 지역은 필수입니다.") + @Min(value = 1, message = "올바른 시도 코드를 입력해주세요.") + private Integer sidoCode; + + @JsonProperty + @NotBlank(message = "현재 기분은 필수입니다.") + @Size(max = 50, message = "기분 설명은 50자 이하로 입력해주세요") + private String mood; + +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/response/AiplannerResponse.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/response/AiplannerResponse.java new file mode 100644 index 0000000..7df0ecb --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/response/AiplannerResponse.java @@ -0,0 +1,44 @@ +package com.swyp.catsgotogedog.aiplanner.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Builder +public class AiplannerResponse { + private List dayPlans; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class DayPlan { + private Integer day; + private List dayContents; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ContentInfo { + private Integer contentId; + private String title; + private String categoryId; + private String addr1; + private String addr2; + private String image; + private String thumbImage; + private double mapx; + private double mapy; + private String rest; + } +} diff --git a/src/main/java/com/swyp/catsgotogedog/aiplanner/service/AiPlannerService.java b/src/main/java/com/swyp/catsgotogedog/aiplanner/service/AiPlannerService.java new file mode 100644 index 0000000..2e5806c --- /dev/null +++ b/src/main/java/com/swyp/catsgotogedog/aiplanner/service/AiPlannerService.java @@ -0,0 +1,414 @@ +package com.swyp.catsgotogedog.aiplanner.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientResponseException; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp.catsgotogedog.User.domain.entity.User; +import com.swyp.catsgotogedog.User.repository.UserRepository; +import com.swyp.catsgotogedog.aiplanner.domain.ContentEmbedding; +import com.swyp.catsgotogedog.aiplanner.repository.ContentEmbeddingRepository; +import com.swyp.catsgotogedog.aiplanner.request.PlannerRequest; +import com.swyp.catsgotogedog.aiplanner.response.AiplannerResponse; +import com.swyp.catsgotogedog.content.domain.entity.Content; +import com.swyp.catsgotogedog.content.repository.ContentRepository; +import com.swyp.catsgotogedog.content.repository.RestaurantInformationRepository; +import com.swyp.catsgotogedog.content.repository.SightsInformationRepository; +import com.swyp.catsgotogedog.global.exception.CatsgotogedogException; +import com.swyp.catsgotogedog.global.exception.ErrorCode; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.Hit; +import co.elastic.clients.json.JsonData; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class AiPlannerService { + + @Value("${clova.api.embed.url}") + private String clovaUrl; + + @Value("${clova.api.key}") + private String apiKey; + + private RestClient restClient; + private final ContentEmbeddingRepository contentEmbeddingRepository; + private final ContentRepository contentRepository; + private final ElasticsearchClient esClient; + private final ObjectMapper objectMapper; + private final UserRepository userRepository; + private final SightsInformationRepository sightsInformationRepository; + private final RestaurantInformationRepository restaurantInformationRepository; + + public AiPlannerService(ContentEmbeddingRepository contentEmbeddingRepository, ContentRepository contentRepository, ElasticsearchClient esClient, ObjectMapper objectMapper, UserRepository userRepository, SightsInformationRepository sightsInformationRepository + , RestaurantInformationRepository restaurantInformationRepository) { + this.contentEmbeddingRepository = contentEmbeddingRepository; + this.contentRepository = contentRepository; + this.esClient = esClient; + this.objectMapper = objectMapper; + this.userRepository = userRepository; + this.sightsInformationRepository = sightsInformationRepository; + this.restaurantInformationRepository = restaurantInformationRepository; + } + + @PostConstruct + private void initRestClient() { + this.restClient = RestClient.builder() + .baseUrl(clovaUrl) + .defaultHeader("Authorization", "Bearer " + apiKey) + .defaultHeader("X-NCP-CLOVASTUDIO-REQUEST-ID", "f729bb278df149ce8b7e25e3ec8e1a25") + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + } + + @Transactional + public void initEmbedContentData() { + int processedCount = 0; + int failedCount = 0; + + // Content 테이블 id 조회 + List allContents = contentRepository.findAll(); + + // ContentId 추출 + List allContentIds = allContents.stream() + .map(Content::getContentId) + .toList(); + + // 현재 삽입되어있는 엘라스틱서치 Content 데이터 목록 + List existEmbeddings = contentEmbeddingRepository.findByContentIdIn(allContentIds); + + // 엘라스틱서치에 존재하는 Id 추출 + Set existIds = existEmbeddings.stream() + .map(ContentEmbedding::getContentId) + .collect(Collectors.toSet()); + + // 존재하지 않는 Content 데이터 추출 + List newContents = allContents.stream() + .filter(content -> !existIds.contains(content.getContentId())) + .filter(this::isValidEmbeddingData) + .toList(); + + List contentEmbeddings = new ArrayList<>(); + + for(Content content : newContents) { + try { + if(processedCount > 0) { + Thread.sleep(1000); + } + + // 임베딩 텍스트 생성 + String textToEmbed = createEmbeddingText(content); + // Clova 호출 + List embedding = callClovaEmbedApi(textToEmbed); + + if(!embedding.isEmpty()) { + ContentEmbedding contentEmbedding = ContentEmbedding.builder() + .contentId(content.getContentId()) + .title(content.getTitle()) + .contentTypeId(content.getContentTypeId()) + .categoryId(content.getCategoryId()) + .sidoCode(content.getSidoCode()) + .sigunguCode(content.getSigunguCode()) + .mapx(content.getMapx()) + .mapy(content.getMapy()) + .embedding(embedding) + .build(); + + contentEmbeddings.add(contentEmbedding); + processedCount++; + + if(contentEmbeddings.size() >= 10) { + saveToElasticSearch(contentEmbeddings); + contentEmbeddings.clear(); + } + + if(processedCount % 10 == 0) { + log.info("임베딩 진행 상황 : {}/{}", processedCount, newContents.size()); + } + } else { + failedCount++; + log.warn("ContentId {} - 임베딩 생성 실패", content.getContentId()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("임베딩 처리 중 인터럽트 발생", e); + break; + } catch(Exception e) { + failedCount++; + log.error("ContentId {} 임베딩 처리 중 오류발생 : {}", content.getContentId(), e.getMessage()); + } + } + + + + + log.info("임베딩 데이터 삽입 완료 - 성공 : {}, 실패 : {}", processedCount, failedCount); + } + + public AiplannerResponse createPlan(String userId, PlannerRequest request) { + validateUser(userId); + + List moodEmbed = getMoodEmbed(request.getMood()); + + if(moodEmbed.isEmpty()) { + throw new CatsgotogedogException(ErrorCode.CLOVA_HASHTAG_SERVER_ERROR); + } + + List dayPlans = new ArrayList<>(); + + int days = request.getDuration().getDays(); + Set excludedIds = new HashSet<>(); + + for(int day = 1; day <= days; day++) { + AiplannerResponse.DayPlan dayPlan = createDayPlan(day, request.getSidoCode(), days, moodEmbed, excludedIds); + dayPlans.add(dayPlan); + } + + return new AiplannerResponse(dayPlans); + } + + private AiplannerResponse.DayPlan createDayPlan( + int day, int sido, int totalDays, List moodEmbed, Set excludedIds + ) { + List dayContents = new ArrayList<>(); + + dayContents.add(findBestPlace(sido, moodEmbed, 12, excludedIds)); + dayContents.add(findBestPlace(sido, moodEmbed, 39, excludedIds)); + dayContents.add(findBestPlace(sido, moodEmbed, 12, excludedIds)); + dayContents.add(findBestPlace(sido, moodEmbed, 39, excludedIds)); + + if (totalDays > 1 && day < totalDays) { + dayContents.add(findBestPlace(sido, moodEmbed, 32, excludedIds)); + } + + return new AiplannerResponse.DayPlan(day, dayContents); + } + + private List getMoodEmbed(String mood) { + + Map requestBody = Map.of("text", mood); + + try { + log.info("{} 요청", clovaUrl); + Map responseBody = Objects.requireNonNull(restClient.post() + .body(requestBody) + .retrieve() + .body(Map.class)); + + log.info("responseBody : {}", responseBody); + + Map result = (Map) responseBody.get("result"); + + if(result != null) { + Object embeddingObject = result.get("embedding"); + log.info(embeddingObject.toString()); + + if (embeddingObject instanceof List) { + List embeddingList = (List) embeddingObject; + + return embeddingList.stream() + .map(Number::floatValue) + .toList(); + } + } + + } catch(RestClientResponseException e) { + log.error("Clova Embed Api 호출 오류 :: 상태코드 {} - 응답 :: {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new CatsgotogedogException(ErrorCode.CLOVA_HASHTAG_SERVER_ERROR); + } catch(Exception e) { + log.error("Mood 임베드 Api 호출 중 예상치 못한 오류 발생", e); + throw new CatsgotogedogException(ErrorCode.INTERNAL_SERVER_ERROR); + } + + return Collections.emptyList(); + } + + private AiplannerResponse.ContentInfo findBestPlace( + Integer sidoCode, + List embededMood, + Integer contentTypeId, + Set excludedIds) { + + try { + List queryVector = embededMood.stream() + .map(Float::doubleValue) + .collect(Collectors.toList()); + + SearchRequest searchRequest = SearchRequest.of(s -> s + .index("content-embedding") + .size(100) + .query(q -> q + .scriptScore(ss -> ss + .query(query -> query + .bool(b -> b + .filter(f -> f.term(t -> t.field("sidoCode").value(sidoCode))) + .filter(f -> f.term(t -> t.field("contentTypeId").value(contentTypeId))) + ) + ) + .script(script -> script + .source("cosineSimilarity(params.queryVector, 'embedding') + 1.0") + .params("queryVector", JsonData.of(queryVector)) + ) + ) + ) + ); + + SearchResponse response = esClient.search(searchRequest, Map.class); + + for(Hit hit : response.hits().hits()) { + log.info("검색된 문서 ID: {}, Score: {}", hit.id(), hit.score()); + + Map sourceAsMap = hit.source(); + + if(sourceAsMap == null) { + log.warn("sourceAsMap이 null입니다."); + continue; + } + + log.info("문서 내용: {}", sourceAsMap); + + Integer contentId = (Integer) sourceAsMap.get("contentId"); + log.info("contentId: {}, excludedIds에 포함여부: {}", + contentId, excludedIds.contains(contentId)); + + if(contentId != null && !excludedIds.contains(contentId)) { + excludedIds.add(contentId); + Content content = contentRepository.findByContentId(contentId); + String rest = null; + + if(content.getContentTypeId() == 12) { + rest = sightsInformationRepository.findRestDateByContentId(content.getContentId()); + } else if(content.getContentTypeId() == 39) { + rest = restaurantInformationRepository.findRestDateByContentId(content.getContentId()); + } + + if(rest == null || rest.isEmpty()) { + rest = null; + } + + if(content == null) { + log.warn("contentId {}에 해당하는 Content를 찾을 수 없습니다.", contentId); + continue; + } + + log.info("추천 장소 선택됨: {}", content.getTitle()); + + return AiplannerResponse.ContentInfo.builder() + .contentId(content.getContentId()) + .image(content.getImage()) + .thumbImage(content.getThumbImage()) + .title(content.getTitle()) + .addr1(content.getAddr1()) + .addr2(content.getAddr2()) + .mapx(content.getMapx()) + .mapy(content.getMapy()) + .categoryId(content.getCategoryId()) + .rest(rest) + .build(); + } + } + + } catch (Exception e) { + log.error("Elasticsearch API 호출 오류 : {}", e.getMessage(), e); + throw new CatsgotogedogException(ErrorCode.INTERNAL_SERVER_ERROR); + } + + throw new CatsgotogedogException(ErrorCode.NO_RECOMMEND_PLACES); + } + + private boolean isValidEmbeddingData(Content content) { + boolean hasTitle = content.getTitle() != null && !content.getTitle().trim().isEmpty(); + boolean hasOverview = content.getOverview() != null && !content.getOverview().trim().isEmpty(); + + if (!hasTitle || !hasOverview) { + log.debug("ContentId {} - title 또는 overview가 모두 없어 임베딩 제외", content.getContentId()); + return false; + } + + return true; + } + + private String createEmbeddingText(Content content) { + StringBuilder sb = new StringBuilder(); + if(content.getTitle() != null) { + sb.append("제목 : ").append(content.getTitle()).append(" "); + } + if(content.getOverview() != null) { + sb.append("소개 : ").append(content.getOverview()).append(" "); + } + return sb.toString().trim(); + } + + private List callClovaEmbedApi(String text) { + Map requestBody = Map.of("text", text); + + try { + Map responseBody = Objects.requireNonNull(restClient.post() + .body(requestBody) + .retrieve() + .body(Map.class)); + + Map result = (Map) responseBody.get("result"); + + if(result != null) { + Object embeddingObject = result.get("embedding"); + log.info(embeddingObject.toString()); + + if (embeddingObject instanceof List) { + List embeddingList = (List) embeddingObject; + + return embeddingList.stream() + .map(Number::floatValue) + .toList(); + } + } + + } catch(RestClientResponseException e) { + log.error("Clova Embed Api 호출 오류 :: 상태코드 {} - 응답 :: {}", e.getStatusCode(), e.getResponseBodyAsString()); + } catch(Exception e) { + log.error("{} Api 호출 중 예상치 못한 오류 발생", clovaUrl, e); + } + + return Collections.emptyList(); + } + + private void saveToElasticSearch(List embeddings) { + try { + contentEmbeddingRepository.saveAll(embeddings); + + log.info("{} 개의 ContentEmbedding 데이터 저장 완료", embeddings.size()); + }catch (Exception e) { + log.error("Elasticsearch 데이터 저장중 오류 발생 : ", e); + throw new RuntimeException("Elasticsaerch 저장중 오류 발생", e); + } + } + + private User validateUser(String userId) { + return userRepository.findById(Integer.parseInt(userId)) + .orElseThrow(() -> new CatsgotogedogException(ErrorCode.MEMBER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/swyp/catsgotogedog/global/exception/ErrorCode.java b/src/main/java/com/swyp/catsgotogedog/global/exception/ErrorCode.java index ac540a5..48451ea 100644 --- a/src/main/java/com/swyp/catsgotogedog/global/exception/ErrorCode.java +++ b/src/main/java/com/swyp/catsgotogedog/global/exception/ErrorCode.java @@ -27,6 +27,7 @@ public enum ErrorCode { REVIEW_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 리뷰 이미지입니다."), SIGUNGU_CODE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 지역을 찾을 수 없습니다."), SIDO_CODE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 시/도를 찾을 수 없습니다."), + NO_RECOMMEND_PLACES(HttpStatus.NOT_FOUND.value(), "추천할 관광지가 없어요."), // 405 Method not allowed METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(), "허용되지 않은 HTTP 메소드입니다."), diff --git a/src/main/resources/elasticsearch/content-embed-mapping.json b/src/main/resources/elasticsearch/content-embed-mapping.json new file mode 100644 index 0000000..20e5947 --- /dev/null +++ b/src/main/resources/elasticsearch/content-embed-mapping.json @@ -0,0 +1,16 @@ +{ + "properties": { + "contentId": { "type": "integer" }, + "contentTypeId": { "type": "integer" }, + "categoryId": { "type": "keyword" }, + "sidoCode": { "type": "integer" }, + "sigunguCode": { "type": "integer" }, + "mapx": { "type": "double" }, + "mapy": { "type": "double" }, + "title": { "type": "keyword" }, + "embedding": { + "type": "dense_vector", + "dims": 1024 + } + } +} \ No newline at end of file diff --git a/src/main/resources/elasticsearch/content-embed-setting.json b/src/main/resources/elasticsearch/content-embed-setting.json new file mode 100644 index 0000000..9761fcd --- /dev/null +++ b/src/main/resources/elasticsearch/content-embed-setting.json @@ -0,0 +1,6 @@ +{ + "index": { + "number_of_shards": 1, + "number_of_replicas": 1 + } +} \ No newline at end of file