From 5a4eb77daf004aa7bc7868622aa2caf56ab9e0c0 Mon Sep 17 00:00:00 2001 From: persi Date: Tue, 27 May 2025 15:33:32 +0900 Subject: [PATCH 01/41] =?UTF-8?q?feature:=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20api=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=B0=8F=20db=20=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/service/TourSpotApiService.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java new file mode 100644 index 0000000..269b0a2 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java @@ -0,0 +1,107 @@ +package com.example.silverbridgeX_user.activity.service; + +import com.example.silverbridgeX_user.activity.domain.Activity; +import com.example.silverbridgeX_user.activity.domain.ActivityType; +import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.TourSpotItemDto; +import com.example.silverbridgeX_user.activity.repository.ActivityNativeRepository; +import com.example.silverbridgeX_user.activity.repository.ActivityRepository; +import com.example.silverbridgeX_user.global.util.EmbeddingClient; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TourSpotApiService { + + private final ActivityRepository activityRepository; + private final EmbeddingClient embeddingClient; + private final ActivityNativeRepository activityNativeRepository; + + @Value("${activity.api.key.festival}") + private String apiKey; + + public void fetchAndSaveTourSpotData() throws Exception { + System.out.println("start"); + StringBuilder urlBuilder = new StringBuilder("http://api.data.go.kr/openapi/tn_pubr_public_trrsrt_api"); + urlBuilder.append("?" + URLEncoder.encode("serviceKey", "UTF-8") + + "=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); + urlBuilder.append("&type=json&pageNo=1&numOfRows=100"); + + URL url = new URL(urlBuilder.toString()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Content-type", "application/json"); + + BufferedReader rd = (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) + ? new BufferedReader(new InputStreamReader(conn.getInputStream())) + : new BufferedReader(new InputStreamReader(conn.getErrorStream())); + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = rd.readLine()) != null) { + sb.append(line); + } + rd.close(); + conn.disconnect(); + + log.info("Raw JSON 응답:\n" + sb.toString()); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode items = objectMapper.readTree(sb.toString()) + .path("response") + .path("body") + .path("items"); + + if (items != null && items.isArray()) { + System.out.println("가져온 관광지 개수: " + items.size()); + for (JsonNode item : items) { + TourSpotItemDto dto = objectMapper.treeToValue(item, TourSpotItemDto.class); + + if (!activityRepository.existsByName(dto.getName())) { + Activity activity = Activity.builder() + .name(dto.getName()) + .description(dto.getDescription()) + .streetAddress(dto.getStreetAddress()) + .lotNumberAddress(dto.getLotNumberAddress()) + .latitude(dto.getLatitude()) + .longitude(dto.getLongitude()) + .startDate(LocalDate.parse(dto.getReferenceDate())) + .endDate(LocalDate.parse("2999-12-31")) + .phoneNumber(dto.getPhoneNumber()) + .activityType(ActivityType.TOUR_SPOT) + .chosen(0L) + .shown(0L) + .CTR(0.0) + .build(); + activityRepository.save(activity); + + String vectorLiteral = embeddingClient.getEmbeddingLiteral( + dto.getDescription()); // "(0.1, 0.2, ..., 0.3)" + activityNativeRepository.updateVectorByName( + dto.getName(), + vectorLiteral + ); + + log.info("저장됨: " + dto.getName()); + } + + + } + } else { + log.info("items가 null이거나 배열이 아님. 실제 응답 확인 필요"); + } + } +} + From a6bbf524122b17f443b154bc8d74626fae5c290e Mon Sep 17 00:00:00 2001 From: persi Date: Tue, 27 May 2025 17:24:02 +0900 Subject: [PATCH 02/41] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=EB=90=9C?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=94=B0=EB=A1=9C=20=EB=B9=BC=EA=B8=B0?= =?UTF-8?q?,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/service/ActivityApiService.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java new file mode 100644 index 0000000..0d62577 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java @@ -0,0 +1,58 @@ +package com.example.silverbridgeX_user.activity.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class ActivityApiService { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Value("${activity.api.key.festival}") + private String apiKey; + + public String buildUrl(String url, int pageNo) throws Exception { + StringBuilder urlBuilder = new StringBuilder(url); + urlBuilder.append("?" + URLEncoder.encode("serviceKey", "UTF-8") + "=" + URLEncoder.encode(apiKey, + StandardCharsets.UTF_8)); + urlBuilder.append("&type=json&pageNo=" + pageNo + "&numOfRows=50"); + return urlBuilder.toString(); + } + + public String getJsonFromUrl(String urlStr) throws Exception { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Content-type", "application/json"); + + BufferedReader rd = (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) + ? new BufferedReader(new InputStreamReader(conn.getInputStream())) + : new BufferedReader(new InputStreamReader(conn.getErrorStream())); + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = rd.readLine()) != null) { + sb.append(line); + } + rd.close(); + conn.disconnect(); + + return sb.toString(); + } + + public JsonNode parseItems(String json) throws Exception { + return objectMapper.readTree(json) + .path("response") + .path("body") + .path("items"); + } + +} From 02dcfb8c0db5aa49b521a5b89a9b2c7e1a0e8a9a Mon Sep 17 00:00:00 2001 From: persi Date: Tue, 27 May 2025 17:29:23 +0900 Subject: [PATCH 03/41] =?UTF-8?q?refactor:=20=EA=B0=80=EC=83=81=20?= =?UTF-8?q?=EC=93=B0=EB=A0=88=EB=93=9C=20=EC=9D=B4=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EA=B0=81=EA=B0=81=EC=9D=98=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/service/FestivalApiService.java | 140 ++++++++---------- .../activity/service/TourSpotApiService.java | 130 +++++++--------- 2 files changed, 118 insertions(+), 152 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java index 004ccf3..3d9f6bb 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java @@ -2,22 +2,17 @@ import com.example.silverbridgeX_user.activity.domain.Activity; import com.example.silverbridgeX_user.activity.domain.ActivityType; -import com.example.silverbridgeX_user.activity.dto.FestivalResponseDto.FestivalItemDto; +import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.FestivalItemDto; import com.example.silverbridgeX_user.activity.repository.ActivityNativeRepository; import com.example.silverbridgeX_user.activity.repository.ActivityRepository; import com.example.silverbridgeX_user.global.util.EmbeddingClient; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.time.LocalDate; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Slf4j @@ -28,84 +23,73 @@ public class FestivalApiService { private final ActivityRepository activityRepository; private final EmbeddingClient embeddingClient; private final ActivityNativeRepository activityNativeRepository; + private final ActivityApiService activityApiService; - @Value("${activity.api.key.festival}") - private String apiKey; - - public void fetchAndSaveFestivalData() throws Exception { - System.out.println("start"); - StringBuilder urlBuilder = new StringBuilder("http://api.data.go.kr/openapi/tn_pubr_public_cltur_fstvl_api"); - urlBuilder.append("?" + URLEncoder.encode("serviceKey", "UTF-8") - + "=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); - urlBuilder.append("&type=json&pageNo=1&numOfRows=10"); - - URL url = new URL(urlBuilder.toString()); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty("Content-type", "application/json"); - - BufferedReader rd = (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) - ? new BufferedReader(new InputStreamReader(conn.getInputStream())) - : new BufferedReader(new InputStreamReader(conn.getErrorStream())); - - StringBuilder sb = new StringBuilder(); - String line; - while ((line = rd.readLine()) != null) { - sb.append(line); - } - rd.close(); - conn.disconnect(); - - log.info("Raw JSON 응답:\n" + sb.toString()); + private final ObjectMapper objectMapper = new ObjectMapper(); - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode items = objectMapper.readTree(sb.toString()) - .path("response") - .path("body") - .path("items"); + String apiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_cltur_fstvl_api"; - if (items != null && items.isArray()) { - System.out.println("가져온 축제 개수: " + items.size()); - for (JsonNode item : items) { - FestivalItemDto dto = objectMapper.treeToValue(item, FestivalItemDto.class); + public void fetchAndSaveFestivalData() { + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - if (LocalDate.parse(dto.getFstvlEndDate()).isBefore(LocalDate.now())) { - continue; - } + int page = 1; + while (true) { + try { + String urlStr = activityApiService.buildUrl(apiUrl, page); + String json = activityApiService.getJsonFromUrl(urlStr); + JsonNode items = activityApiService.parseItems(json); - if (!activityRepository.existsByName(dto.getName())) { - Activity activity = Activity.builder() - .name(dto.getName()) - .description(dto.getDescription()) - .streetAddress(dto.getStreetAddress()) - .lotNumberAddress(dto.getLotNumberAddress()) - .latitude(dto.getLatitude()) - .longitude(dto.getLongitude()) - .startDate(LocalDate.parse(dto.getFstvlStartDate())) - .endDate(LocalDate.parse(dto.getFstvlEndDate())) - .homepageUrl(dto.getHomepageUrl()) - .phoneNumber(dto.getPhoneNumber()) - .activityType(ActivityType.FESTIVAL) - .chosen(0L) - .shown(0L) - .CTR(0.0) - .build(); - activityRepository.save(activity); - - String vectorLiteral = embeddingClient.getEmbeddingLiteral( - dto.getDescription()); // "(0.1, 0.2, ..., 0.3)" - activityNativeRepository.updateVectorByName( - dto.getName(), - vectorLiteral - ); - - log.info("저장됨: " + dto.getName()); + if (items == null || !items.isArray() || items.isEmpty()) { + log.info("page {} 응답 없음. 중단.", page); + return; } - + executor.submit(() -> { + for (JsonNode item : items) { + try { + FestivalItemDto dto = objectMapper.treeToValue(item, FestivalItemDto.class); + + if (LocalDate.parse(dto.getFstvlEndDate()).isBefore(LocalDate.now())) { + continue; + } + + if (!activityRepository.existsByName(dto.getName())) { + Activity activity = Activity.builder() + .name(dto.getName()) + .description(dto.getDescription()) + .streetAddress(dto.getStreetAddress()) + .lotNumberAddress(dto.getLotNumberAddress()) + .latitude(dto.getLatitude()) + .longitude(dto.getLongitude()) + .startDate(LocalDate.parse(dto.getFstvlStartDate())) + .endDate(LocalDate.parse(dto.getFstvlEndDate())) + .homepageUrl(dto.getHomepageUrl()) + .phoneNumber(dto.getPhoneNumber()) + .activityType(ActivityType.FESTIVAL) + .chosen(0L) + .shown(0L) + .CTR(0.0) + .build(); + activityRepository.save(activity); + + String vectorLiteral = embeddingClient.getEmbeddingLiteral(dto.getDescription()); + activityNativeRepository.updateVectorByName(dto.getName(), vectorLiteral); + + log.info("저장됨: " + dto.getName()); + } + + } catch (Exception e) { + log.error("항목 처리 중 오류 발생", e); + } + } + }); + page++; + Thread.sleep(100); // 속도 제한 + + } catch (Exception e) { + log.error("page {} 처리 중 오류 발생", page, e); + break; } - } else { - log.info("items가 null이거나 배열이 아님. 실제 응답 확인 필요"); } } } diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java index 269b0a2..a56982d 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java @@ -8,16 +8,11 @@ import com.example.silverbridgeX_user.global.util.EmbeddingClient; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.time.LocalDate; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Slf4j @@ -28,80 +23,67 @@ public class TourSpotApiService { private final ActivityRepository activityRepository; private final EmbeddingClient embeddingClient; private final ActivityNativeRepository activityNativeRepository; + private final ActivityApiService activityApiService; - @Value("${activity.api.key.festival}") - private String apiKey; + private final ObjectMapper objectMapper = new ObjectMapper(); + String apiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_trrsrt_api"; - public void fetchAndSaveTourSpotData() throws Exception { - System.out.println("start"); - StringBuilder urlBuilder = new StringBuilder("http://api.data.go.kr/openapi/tn_pubr_public_trrsrt_api"); - urlBuilder.append("?" + URLEncoder.encode("serviceKey", "UTF-8") - + "=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); - urlBuilder.append("&type=json&pageNo=1&numOfRows=100"); + public void fetchAndSaveTourSpotData() { + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - URL url = new URL(urlBuilder.toString()); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty("Content-type", "application/json"); + int page = 1; + while (true) { + try { + String urlStr = activityApiService.buildUrl(apiUrl, page); + String json = activityApiService.getJsonFromUrl(urlStr); + JsonNode items = activityApiService.parseItems(json); - BufferedReader rd = (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) - ? new BufferedReader(new InputStreamReader(conn.getInputStream())) - : new BufferedReader(new InputStreamReader(conn.getErrorStream())); - - StringBuilder sb = new StringBuilder(); - String line; - while ((line = rd.readLine()) != null) { - sb.append(line); - } - rd.close(); - conn.disconnect(); - - log.info("Raw JSON 응답:\n" + sb.toString()); - - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode items = objectMapper.readTree(sb.toString()) - .path("response") - .path("body") - .path("items"); - - if (items != null && items.isArray()) { - System.out.println("가져온 관광지 개수: " + items.size()); - for (JsonNode item : items) { - TourSpotItemDto dto = objectMapper.treeToValue(item, TourSpotItemDto.class); - - if (!activityRepository.existsByName(dto.getName())) { - Activity activity = Activity.builder() - .name(dto.getName()) - .description(dto.getDescription()) - .streetAddress(dto.getStreetAddress()) - .lotNumberAddress(dto.getLotNumberAddress()) - .latitude(dto.getLatitude()) - .longitude(dto.getLongitude()) - .startDate(LocalDate.parse(dto.getReferenceDate())) - .endDate(LocalDate.parse("2999-12-31")) - .phoneNumber(dto.getPhoneNumber()) - .activityType(ActivityType.TOUR_SPOT) - .chosen(0L) - .shown(0L) - .CTR(0.0) - .build(); - activityRepository.save(activity); - - String vectorLiteral = embeddingClient.getEmbeddingLiteral( - dto.getDescription()); // "(0.1, 0.2, ..., 0.3)" - activityNativeRepository.updateVectorByName( - dto.getName(), - vectorLiteral - ); - - log.info("저장됨: " + dto.getName()); + if (items == null || !items.isArray() || items.isEmpty()) { + log.info("page {} 응답 없음. 중단.", page); + return; } - + executor.submit(() -> { + for (JsonNode item : items) { + try { + TourSpotItemDto dto = objectMapper.treeToValue(item, TourSpotItemDto.class); + + if (!activityRepository.existsByName(dto.getName())) { + Activity activity = Activity.builder() + .name(dto.getName()) + .description(dto.getDescription()) + .streetAddress(dto.getStreetAddress()) + .lotNumberAddress(dto.getLotNumberAddress()) + .latitude(dto.getLatitude()) + .longitude(dto.getLongitude()) + .startDate(LocalDate.parse(dto.getReferenceDate())) + .endDate(LocalDate.parse("2999-12-31")) + .phoneNumber(dto.getPhoneNumber()) + .activityType(ActivityType.TOUR_SPOT) + .chosen(0L) + .shown(0L) + .CTR(0.0) + .build(); + activityRepository.save(activity); + + String vectorLiteral = embeddingClient.getEmbeddingLiteral(dto.getDescription()); + activityNativeRepository.updateVectorByName(dto.getName(), vectorLiteral); + + log.info("저장됨: {}", dto.getName()); + } + } catch (Exception e) { + log.error("항목 처리 중 오류 발생", e); + } + } + }); + page++; + Thread.sleep(100); // 속도 제한 + + } catch (Exception e) { + log.error("page {} 처리 중 오류 발생", page, e); + break; } - } else { - log.info("items가 null이거나 배열이 아님. 실제 응답 확인 필요"); } } -} +} From 973129c494914871720b3dfdc32c61a0d2ce1ff9 Mon Sep 17 00:00:00 2001 From: persi Date: Tue, 27 May 2025 17:30:13 +0900 Subject: [PATCH 04/41] =?UTF-8?q?core:=20=ED=99=9C=EB=8F=99=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=EC=BB=AC=EB=9F=BC=20=EA=B8=B8=EC=9D=B4=201000?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=86=92=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/silverbridgeX_user/activity/domain/Activity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index e8f41ab..77dc159 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -27,10 +27,11 @@ public class Activity { private ActivityType activityType; + @Column(length = 1000) private String description; @Column(columnDefinition = "vector(384)") - @Transient // JPA 저장 시 무시 + @Transient private String descriptionEmbedding; private String streetAddress; From 687565cbc3f7d009cdd4bf04b4cc18734450d3b3 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 12:42:29 +0900 Subject: [PATCH 05/41] =?UTF-8?q?core:=20=ED=99=9C=EB=8F=99=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20api=20=EC=9D=91=EB=8B=B5=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/dto/ActivityResponseDto.java | 150 ++++++++++++++++++ .../activity/dto/FestivalResponseDto.java | 76 --------- 2 files changed, 150 insertions(+), 76 deletions(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java delete mode 100644 src/main/java/com/example/silverbridgeX_user/activity/dto/FestivalResponseDto.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java b/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java new file mode 100644 index 0000000..d540ca5 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java @@ -0,0 +1,150 @@ +package com.example.silverbridgeX_user.activity.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@ToString +public class ActivityResponseDto { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @ToString + public static class FestivalItemDto { + @JsonProperty("fstvlNm") + private String name; // 축제명 + + @JsonProperty("fstvlCo") + private String description; // 축제 내용 + + @JsonProperty("rdnmadr") + private String streetAddress; // 도로명주소 + + @JsonProperty("lnmadr") + private String lotNumberAddress; // 지번 주소 + + @JsonProperty("latitude") + private String latitude; // 위도 + + @JsonProperty("longitude") + private String longitude; // 경도 + + @JsonProperty("homepageUrl") + private String homepageUrl; // 홈페이지 주소 + + @JsonProperty("phoneNumber") + private String phoneNumber; // 전화번호 + + @JsonProperty("relateInfo") + private String relateInfo; // 관련 정보 + + @JsonProperty("fstvlStartDate") + private String fstvlStartDate; // 축제 시작 일자 + + @JsonProperty("fstvlEndDate") + private String fstvlEndDate; // 축제 종료 일자 + + @JsonProperty("mnnstNm") + private String hostOrg1; // 주관기관명 + + @JsonProperty("auspcInsttNm") + private String hostOrg2; // 후최기관명 + + @JsonProperty("suprtInsttNm") + private String support; // 후원기관명 + + @JsonProperty("referenceDate") + private String referenceDate; // 데이터 기준 일자 + + @JsonProperty("insttCode") + private String insttCode; // 제공기관 코드 + + @JsonProperty("insttNm") + private String insttNm; // 제공기관 기관병 + + @JsonProperty("opar") + private String opar; // 개최장소 + + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @ToString + public static class TourSpotItemDto { + @JsonProperty("trrsrtNm") + private String name; // 관광지명 + + @JsonProperty("trrsrtIntrcn") + private String description; // 관광지 소개 + + @JsonProperty("rdnmadr") + private String streetAddress; // 도로명 주소 + + @JsonProperty("lnmadr") + private String lotNumberAddress; // 지번 주소 + + @JsonProperty("latitude") + private String latitude; // 위도 + + @JsonProperty("longitude") + private String longitude; // 경도 + + @JsonProperty("ar") + private String area; // 면적 + + @JsonProperty("trrsrtSe") + private String classification; // 관광지 구분 + + @JsonProperty("phoneNumber") + private String phoneNumber; // 관리기관 전화번호 + + @JsonProperty("institutionNm") + private String institutionNm; // 관리기관명 + + @JsonProperty("insttNm") + private String insttNm; // 관리기관명 + + @JsonProperty("referenceDate") + private String referenceDate; // 데이터 기준 일자 + + @JsonProperty("insttCode") + private String insttCode; // 제공기관 코드 + + @JsonProperty("cnvnncFclty") + private String cnvnncFclty; // 공공 편익 시설 정보 + + @JsonProperty("recrtClturFclty") + private String recrtClturFclty; // 휴양 및 문화시설 정보 + + @JsonProperty("stayngInfo") + private String stayngInfo; // 숙박 시설 정보 + + @JsonProperty("mvmAmsmtFclty") + private String mvmAmsmtFclty; // 운동 및 오락시설 정보 + + @JsonProperty("hospitalityFclty") + private String hospitalityFclty; // 접객 시설 정보 + + @JsonProperty("sportFclty") + private String sportFclty; // 지원 시설 정보 + + @JsonProperty("appnDate") + private String appnDate; // 지정 일자 + + @JsonProperty("aceptncCo") + private String aceptncCo; // 수용 인원 수 + + @JsonProperty("prkplceCo") + private String prkplceCo; // 주차 가능 수 + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/silverbridgeX_user/activity/dto/FestivalResponseDto.java b/src/main/java/com/example/silverbridgeX_user/activity/dto/FestivalResponseDto.java deleted file mode 100644 index a8b9b2a..0000000 --- a/src/main/java/com/example/silverbridgeX_user/activity/dto/FestivalResponseDto.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.example.silverbridgeX_user.activity.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@NoArgsConstructor -@ToString -public class FestivalResponseDto { - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - @ToString - public static class FestivalItemDto { - @JsonProperty("fstvlNm") - private String name; - - @JsonProperty("fstvlCo") - private String description; - - @JsonProperty("rdnmadr") - private String streetAddress; - - @JsonProperty("latitude") - private String latitude; - - @JsonProperty("longitude") - private String longitude; - - @JsonProperty("homepageUrl") - private String homepageUrl; - - @JsonProperty("phoneNumber") - private String phoneNumber; - - @JsonProperty("fstvlStartDate") - private String fstvlStartDate; - - @JsonProperty("fstvlEndDate") - private String fstvlEndDate; - - @JsonProperty("mnnstNm") - private String hostOrg1; - - @JsonProperty("auspcInsttNm") - private String hostOrg2; - - @JsonProperty("suprtInsttNm") - private String support; - - @JsonProperty("relateInfo") - private String relateInfo; - - @JsonProperty("lnmadr") - private String lotNumberAddress; - - @JsonProperty("referenceDate") - private String referenceDate; - - @JsonProperty("insttCode") - private String insttCode; - - @JsonProperty("insttNm") - private String insttNm; - - @JsonProperty("opar") - private String opar; - - } -} From d3038a3464704956059fe356ea8e4d6d13f16f1a Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 12:43:53 +0900 Subject: [PATCH 06/41] =?UTF-8?q?core:=20activity=20type=20enum=EC=97=90?= =?UTF-8?q?=20tour=5Fspot=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/silverbridgeX_user/activity/domain/ActivityType.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityType.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityType.java index 66a395f..913e028 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityType.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityType.java @@ -2,5 +2,6 @@ public enum ActivityType { FESTIVAL, + TOUR_SPOT } From 28aa954cf37b4a06ed762457a344c44586d47dee Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 14:14:10 +0900 Subject: [PATCH 07/41] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20=EC=9C=84=EB=8F=84,?= =?UTF-8?q?=20=EA=B2=BD=EB=8F=84=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/silverbridgeX_user/user/domain/User.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index bbaf494..bd18984 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -41,6 +41,8 @@ public class User extends BaseEntity { private String latitude; + private String longitude; + private String profileImage; public User(String username, String nickname, String email) { From dff4641e19c05fb3f950b9099ae749f4f315e765 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 15:57:22 +0900 Subject: [PATCH 08/41] =?UTF-8?q?feature:=20=EC=A3=BC=EC=86=8C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=9C=84=EB=8F=84,=20=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EC=A2=8C=ED=91=9C=20=EA=B3=84=EC=82=B0=20=ED=9B=84?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/Activity.java | 13 ++ .../activity/service/ActivityApiService.java | 6 + .../activity/service/AddressToCoordinate.java | 112 ++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index 77dc159..4202fdd 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -56,4 +56,17 @@ public class Activity { private Double CTR; + public void updateCoordinate(String longitude, String latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + public void updateLongitude(String longitude) { + this.longitude = longitude; + } + + public void updateLatitude(String latitude) { + this.latitude = latitude; + } + } diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java index 0d62577..8e81a5b 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java @@ -55,4 +55,10 @@ public JsonNode parseItems(String json) throws Exception { .path("items"); } + public JsonNode parsePoint(String json) throws Exception { + return objectMapper.readTree(json) + .path("response") + .path("result") + .path("point"); + } } diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java b/src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java new file mode 100644 index 0000000..d9f0eee --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java @@ -0,0 +1,112 @@ +package com.example.silverbridgeX_user.activity.service; + +import com.example.silverbridgeX_user.activity.domain.Activity; +import com.example.silverbridgeX_user.activity.repository.ActivityRepository; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AddressToCoordinate { + private final ActivityRepository activityRepository; + private final ActivityApiService activityApiService; + String key = ""; + String url = "https://api.vworld.kr/req/address?service=address&format=json&request=getCoord&refine=false"; + + public String buildUrl(String type, String address) throws Exception { + StringBuilder urlBuilder = new StringBuilder(url); + urlBuilder.append("&key=" + key); + urlBuilder.append("&type=" + type); // PARCEL : 지번주소, ROAD : 도로명주소 + urlBuilder.append("&address=" + URLEncoder.encode("경상북도 영주시 봉현면 오현리 763-1", StandardCharsets.UTF_8)); + return urlBuilder.toString(); + } + + public void fetchAndSaveCoordinate() throws Exception { + List activities = activityRepository.findAll(); + System.out.println(activities.size()); + + for (Activity activity : activities) { + + try { + if (activity.getLatitude().startsWith("\"")) { + change(activity); + } + + if (!activity.getLongitude().isEmpty() || !activity.getLatitude().isEmpty()) { + continue; + } + + String urlStr = null; + + if (activity.getStreetAddress() != null) { + urlStr = buildUrl("ROAD", activity.getStreetAddress()); + } else if (activity.getLotNumberAddress() != null) { + urlStr = buildUrl("PARCEL", activity.getLotNumberAddress()); + } + + if (urlStr == null) { + continue; + } + + String json = activityApiService.getJsonFromUrl(urlStr); + System.out.println(json); + JsonNode point = activityApiService.parsePoint(json); + + if (!point.isMissingNode()) { + String x = String.valueOf(point.get("x")); + String y = String.valueOf(point.get("y")); + + activity.updateCoordinate(x, y); + activityRepository.save(activity); + } + } catch (IOException | ParseException e) { + throw new RuntimeException(e); + } + + } + + } + + private void change(Activity activity) { + String latitude = activity.getLatitude(); + String longitude = activity.getLongitude(); + + boolean updated = false; + + if (latitude != null && latitude.contains("\"")) { + latitude = latitude.replace("\"", ""); // 모든 " 제거 + activity.updateLatitude(latitude); + updated = true; + } + + if (longitude != null && longitude.contains("\"")) { + longitude = longitude.replace("\"", ""); + activity.updateLongitude(longitude); + updated = true; + } + + if (updated) { + activityRepository.save(activity); + System.out.println("좌표 문자열 정제 완료: " + activity.getId()); + } + } + + // 해당 API에서 제공하지 않는 주소들에 대한 2차 검증 로직 +// public void test() throws Exception { +// Activity activity = activityRepository.findById(Long.valueOf(102)) +// .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); +// String urlStr = buildUrl("PARCEL", activity.getLotNumberAddress()); +// String json = activityApiService.getJsonFromUrl(urlStr); +// System.out.println(json); +// +// } + +} From 41d0f46ba40b44a0dc76f47ac824b70ec380f3b2 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 15:58:09 +0900 Subject: [PATCH 09/41] =?UTF-8?q?=20core:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EC=96=B4=EC=A7=84=20persona=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/service/ActivityService.java | 29 --------------- .../user/domain/Persona.java | 35 ------------------- .../user/repository/PersonaRepository.java | 7 ---- 3 files changed, 71 deletions(-) delete mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java delete mode 100644 src/main/java/com/example/silverbridgeX_user/user/domain/Persona.java delete mode 100644 src/main/java/com/example/silverbridgeX_user/user/repository/PersonaRepository.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java deleted file mode 100644 index b63a07e..0000000 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.silverbridgeX_user.activity.service; - -import com.example.silverbridgeX_user.activity.converter.ActivityLogConverter; -import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.activity.domain.ActivityLog; -import com.example.silverbridgeX_user.activity.repository.ActivityLogRepository; -import com.example.silverbridgeX_user.activity.repository.ActivityRepository; -import com.example.silverbridgeX_user.global.api_payload.ErrorCode; -import com.example.silverbridgeX_user.global.exception.GeneralException; -import com.example.silverbridgeX_user.user.converter.UserConverter; -import com.example.silverbridgeX_user.user.domain.User; -import java.time.LocalDateTime; -import java.util.Map; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ActivityService { - - - - -} diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/Persona.java b/src/main/java/com/example/silverbridgeX_user/user/domain/Persona.java deleted file mode 100644 index fdb77da..0000000 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/Persona.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.silverbridgeX_user.user.domain; - -import jakarta.persistence.*; -import lombok.*; -import java.util.List; -import org.hibernate.type.SqlTypes; -import org.hibernate.annotations.JdbcTypeCode; - -@Entity -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Table(name = "persona") -public class Persona { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne - private User user; - - private String tone; - - @JdbcTypeCode(SqlTypes.JSON) - private List preferredKeywords; - - @Column(name = "preferred_embedding", columnDefinition = "vector(384)") - private String preferredEmbedding; - - @JdbcTypeCode(SqlTypes.JSON) - private List dislikeKeywords; - -} diff --git a/src/main/java/com/example/silverbridgeX_user/user/repository/PersonaRepository.java b/src/main/java/com/example/silverbridgeX_user/user/repository/PersonaRepository.java deleted file mode 100644 index d6f3e01..0000000 --- a/src/main/java/com/example/silverbridgeX_user/user/repository/PersonaRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.silverbridgeX_user.user.repository; - -import com.example.silverbridgeX_user.user.domain.Persona; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PersonaRepository extends JpaRepository { -} From d8e7a88d3f9d27ea5aa87c1d7671c74c0ef30a4d Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 15:59:25 +0900 Subject: [PATCH 10/41] =?UTF-8?q?refactor:=20user=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EC=84=A0=ED=98=B8/=EB=B9=84=EC=84=A0?= =?UTF-8?q?=ED=98=B8=20=ED=82=A4=EC=9B=8C=EB=93=9C=EC=99=80=20=EC=9E=84?= =?UTF-8?q?=EB=B2=A0=EB=94=A9=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/user/domain/User.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index bd18984..1fcd35b 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -7,11 +7,15 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; @Entity @Getter @@ -45,6 +49,20 @@ public class User extends BaseEntity { private String profileImage; + @JdbcTypeCode(SqlTypes.JSON) + private List preferredKeywords; + + @JdbcTypeCode(SqlTypes.JSON) + private List dislikeKeywords; + + @Column(columnDefinition = "vector(384)") + @Transient + private String preferredEmbedding; + + @Column(columnDefinition = "vector(384)") + @Transient + private String dislikeEmbedding; + public User(String username, String nickname, String email) { this.username = username; this.nickname = nickname; From 218bb2dbea5e5e5947c72fb93496a93a18fd2d48 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 16:49:16 +0900 Subject: [PATCH 11/41] =?UTF-8?q?feature:=20=EC=A0=80=EC=9E=A5=ED=95=9C=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=EB=93=A4=20=EB=85=B8=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EC=96=B4=20neo4j=EC=97=90=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/service/ActivityService.java | 288 ++++++++++++++++++ .../activity/service/AddressToCoordinate.java | 112 ------- .../activity/service/FestivalApiService.java | 96 ------ .../activity/service/TourSpotApiService.java | 89 ------ 4 files changed, 288 insertions(+), 297 deletions(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java delete mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java delete mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java delete mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java new file mode 100644 index 0000000..0839815 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -0,0 +1,288 @@ +package com.example.silverbridgeX_user.activity.service; + +import com.example.silverbridgeX_user.activity.domain.Activity; +import com.example.silverbridgeX_user.activity.domain.ActivityType; +import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.FestivalItemDto; +import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.TourSpotItemDto; +import com.example.silverbridgeX_user.activity.repository.ActivityNativeRepository; +import com.example.silverbridgeX_user.activity.repository.ActivityRepository; +import com.example.silverbridgeX_user.global.api_payload.ErrorCode; +import com.example.silverbridgeX_user.global.exception.GeneralException; +import com.example.silverbridgeX_user.global.util.EmbeddingClient; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.neo4j.driver.Session; +import org.neo4j.driver.Values; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ActivityService { + private final ActivityRepository activityRepository; + private final EmbeddingClient embeddingClient; + private final ActivityNativeRepository activityNativeRepository; + private final ActivityApiService activityApiService; + + private final org.neo4j.driver.Driver driver; + private final ObjectMapper objectMapper = new ObjectMapper(); + + String festivalApiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_cltur_fstvl_api"; + String tourSpotApiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_trrsrt_api"; + String addressToCoordinateApiurl = "https://api.vworld.kr/req/address?service=address&format=json&request=getCoord&refine=false"; + + public void fetchAndSaveFestivalData() { + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + + int page = 1; + while (true) { + try { + String urlStr = activityApiService.buildUrl(festivalApiUrl, page); + String json = activityApiService.getJsonFromUrl(urlStr); + JsonNode items = activityApiService.parseItems(json); + + if (items == null || !items.isArray() || items.isEmpty()) { + log.info("page {} 응답 없음. 중단.", page); + return; + } + + executor.submit(() -> { + for (JsonNode item : items) { + try { + FestivalItemDto dto = objectMapper.treeToValue(item, FestivalItemDto.class); + + if (LocalDate.parse(dto.getFstvlEndDate()).isBefore(LocalDate.now())) { + continue; + } + + if (!activityRepository.existsByName(dto.getName())) { + Activity activity = Activity.builder() + .name(dto.getName()) + .description(dto.getDescription()) + .streetAddress(dto.getStreetAddress()) + .lotNumberAddress(dto.getLotNumberAddress()) + .latitude(dto.getLatitude()) + .longitude(dto.getLongitude()) + .startDate(LocalDate.parse(dto.getFstvlStartDate())) + .endDate(LocalDate.parse(dto.getFstvlEndDate())) + .homepageUrl(dto.getHomepageUrl()) + .phoneNumber(dto.getPhoneNumber()) + .activityType(ActivityType.FESTIVAL) + .chosen(0L) + .shown(0L) + .CTR(0.0) + .build(); + activityRepository.save(activity); + + String vectorLiteral = embeddingClient.getEmbeddingLiteral(dto.getDescription()); + activityNativeRepository.updateVectorByName(dto.getName(), vectorLiteral); + + log.info("저장됨: " + dto.getName()); + } + + } catch (Exception e) { + log.error("항목 처리 중 오류 발생", e); + } + } + }); + page++; + Thread.sleep(100); // 속도 제한 + + } catch (Exception e) { + log.error("page {} 처리 중 오류 발생", page, e); + break; + } + } + } + + public void fetchAndSaveTourSpotData() { + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + + int page = 1; + while (true) { + try { + String urlStr = activityApiService.buildUrl(tourSpotApiUrl, page); + String json = activityApiService.getJsonFromUrl(urlStr); + JsonNode items = activityApiService.parseItems(json); + + if (items == null || !items.isArray() || items.isEmpty()) { + log.info("page {} 응답 없음. 중단.", page); + return; + } + + executor.submit(() -> { + for (JsonNode item : items) { + try { + TourSpotItemDto dto = objectMapper.treeToValue(item, TourSpotItemDto.class); + + if (!activityRepository.existsByName(dto.getName())) { + Activity activity = Activity.builder() + .name(dto.getName()) + .description(dto.getDescription()) + .streetAddress(dto.getStreetAddress()) + .lotNumberAddress(dto.getLotNumberAddress()) + .latitude(dto.getLatitude()) + .longitude(dto.getLongitude()) + .startDate(LocalDate.parse(dto.getReferenceDate())) + .endDate(LocalDate.parse("2999-12-31")) + .phoneNumber(dto.getPhoneNumber()) + .activityType(ActivityType.TOUR_SPOT) + .chosen(0L) + .shown(0L) + .CTR(0.0) + .build(); + activityRepository.save(activity); + + String vectorLiteral = embeddingClient.getEmbeddingLiteral(dto.getDescription()); + activityNativeRepository.updateVectorByName(dto.getName(), vectorLiteral); + + log.info("저장됨: {}", dto.getName()); + } + } catch (Exception e) { + log.error("항목 처리 중 오류 발생", e); + } + } + }); + page++; + Thread.sleep(100); // 속도 제한 + + } catch (Exception e) { + log.error("page {} 처리 중 오류 발생", page, e); + break; + } + } + } + + public void fetchAndSaveCoordinate() throws Exception { + List activities = activityRepository.findAll(); + System.out.println(activities.size()); + + for (Activity activity : activities) { + + try { + if (activity.getLatitude().startsWith("\"")) { + change(activity); + } + + if (!activity.getLongitude().isEmpty() || !activity.getLatitude().isEmpty()) { + continue; + } + + String urlStr = null; + + if (activity.getStreetAddress() != null) { + urlStr = buildCoordinateUrl("ROAD", activity.getStreetAddress()); + } else if (activity.getLotNumberAddress() != null) { + urlStr = buildCoordinateUrl("PARCEL", activity.getLotNumberAddress()); + } + + if (urlStr == null) { + continue; + } + + String json = activityApiService.getJsonFromUrl(urlStr); + System.out.println(json); + JsonNode point = activityApiService.parsePoint(json); + + if (!point.isMissingNode()) { + String x = String.valueOf(point.get("x")); + String y = String.valueOf(point.get("y")); + + activity.updateCoordinate(x, y); + activityRepository.save(activity); + } + } catch (IOException | ParseException e) { + throw new RuntimeException(e); + } + + } + + } + + String key = ""; + + private String buildCoordinateUrl(String type, String address) throws Exception { + StringBuilder urlBuilder = new StringBuilder(addressToCoordinateApiurl); + urlBuilder.append("&key=" + key); + urlBuilder.append("&type=" + type); // PARCEL : 지번주소, ROAD : 도로명주소 + urlBuilder.append("&address=" + URLEncoder.encode("경상북도 영주시 봉현면 오현리 763-1", StandardCharsets.UTF_8)); + return urlBuilder.toString(); + } + + private void change(Activity activity) { + String latitude = activity.getLatitude(); + String longitude = activity.getLongitude(); + + boolean updated = false; + + if (latitude != null && latitude.contains("\"")) { + latitude = latitude.replace("\"", ""); // 모든 " 제거 + activity.updateLatitude(latitude); + updated = true; + } + + if (longitude != null && longitude.contains("\"")) { + longitude = longitude.replace("\"", ""); + activity.updateLongitude(longitude); + updated = true; + } + + if (updated) { + activityRepository.save(activity); + System.out.println("좌표 문자열 정제 완료: " + activity.getId()); + } + } + + // 해당 API에서 제공하지 않는 주소들에 대한 2차 검증 로직 + public void test() throws Exception { + Activity activity = activityRepository.findById(Long.valueOf(102)) + .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); + String urlStr = buildCoordinateUrl("PARCEL", activity.getLotNumberAddress()); + String json = activityApiService.getJsonFromUrl(urlStr); + System.out.println(json); + + } + + public void insertActivitiesNeo4j() { + List activities = activityRepository.findAll(); + + for (Activity activity : activities) { + try (Session session = driver.session()) { + session.writeTransaction(tx -> { + tx.run(""" + MERGE (a:Activity {id: $id}) + SET a.name = $name, + a.description = $description, + a.embedding = $embedding, + a.latitude = $latitude, + a.longitude = $longitude, + a.startDate = $startDate, + a.endDate = $endDate + """, + Values.parameters( + "id", activity.getId(), + "name", activity.getName(), + "description", activity.getDescription(), + "embedding", activity.getDescriptionEmbedding(), + "latitude", activity.getLatitude(), + "longitude", activity.getLongitude(), + "startDate", activity.getStartDate(), + "endDate", activity.getEndDate() + )); + return null; + }); + } + } + } + +} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java b/src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java deleted file mode 100644 index d9f0eee..0000000 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/AddressToCoordinate.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.example.silverbridgeX_user.activity.service; - -import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.activity.repository.ActivityRepository; -import com.fasterxml.jackson.databind.JsonNode; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.text.ParseException; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class AddressToCoordinate { - private final ActivityRepository activityRepository; - private final ActivityApiService activityApiService; - String key = ""; - String url = "https://api.vworld.kr/req/address?service=address&format=json&request=getCoord&refine=false"; - - public String buildUrl(String type, String address) throws Exception { - StringBuilder urlBuilder = new StringBuilder(url); - urlBuilder.append("&key=" + key); - urlBuilder.append("&type=" + type); // PARCEL : 지번주소, ROAD : 도로명주소 - urlBuilder.append("&address=" + URLEncoder.encode("경상북도 영주시 봉현면 오현리 763-1", StandardCharsets.UTF_8)); - return urlBuilder.toString(); - } - - public void fetchAndSaveCoordinate() throws Exception { - List activities = activityRepository.findAll(); - System.out.println(activities.size()); - - for (Activity activity : activities) { - - try { - if (activity.getLatitude().startsWith("\"")) { - change(activity); - } - - if (!activity.getLongitude().isEmpty() || !activity.getLatitude().isEmpty()) { - continue; - } - - String urlStr = null; - - if (activity.getStreetAddress() != null) { - urlStr = buildUrl("ROAD", activity.getStreetAddress()); - } else if (activity.getLotNumberAddress() != null) { - urlStr = buildUrl("PARCEL", activity.getLotNumberAddress()); - } - - if (urlStr == null) { - continue; - } - - String json = activityApiService.getJsonFromUrl(urlStr); - System.out.println(json); - JsonNode point = activityApiService.parsePoint(json); - - if (!point.isMissingNode()) { - String x = String.valueOf(point.get("x")); - String y = String.valueOf(point.get("y")); - - activity.updateCoordinate(x, y); - activityRepository.save(activity); - } - } catch (IOException | ParseException e) { - throw new RuntimeException(e); - } - - } - - } - - private void change(Activity activity) { - String latitude = activity.getLatitude(); - String longitude = activity.getLongitude(); - - boolean updated = false; - - if (latitude != null && latitude.contains("\"")) { - latitude = latitude.replace("\"", ""); // 모든 " 제거 - activity.updateLatitude(latitude); - updated = true; - } - - if (longitude != null && longitude.contains("\"")) { - longitude = longitude.replace("\"", ""); - activity.updateLongitude(longitude); - updated = true; - } - - if (updated) { - activityRepository.save(activity); - System.out.println("좌표 문자열 정제 완료: " + activity.getId()); - } - } - - // 해당 API에서 제공하지 않는 주소들에 대한 2차 검증 로직 -// public void test() throws Exception { -// Activity activity = activityRepository.findById(Long.valueOf(102)) -// .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); -// String urlStr = buildUrl("PARCEL", activity.getLotNumberAddress()); -// String json = activityApiService.getJsonFromUrl(urlStr); -// System.out.println(json); -// -// } - -} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java deleted file mode 100644 index 3d9f6bb..0000000 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/FestivalApiService.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.silverbridgeX_user.activity.service; - -import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.activity.domain.ActivityType; -import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.FestivalItemDto; -import com.example.silverbridgeX_user.activity.repository.ActivityNativeRepository; -import com.example.silverbridgeX_user.activity.repository.ActivityRepository; -import com.example.silverbridgeX_user.global.util.EmbeddingClient; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.LocalDate; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class FestivalApiService { - - private final ActivityRepository activityRepository; - private final EmbeddingClient embeddingClient; - private final ActivityNativeRepository activityNativeRepository; - private final ActivityApiService activityApiService; - - private final ObjectMapper objectMapper = new ObjectMapper(); - - String apiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_cltur_fstvl_api"; - - public void fetchAndSaveFestivalData() { - ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - - int page = 1; - while (true) { - try { - String urlStr = activityApiService.buildUrl(apiUrl, page); - String json = activityApiService.getJsonFromUrl(urlStr); - JsonNode items = activityApiService.parseItems(json); - - if (items == null || !items.isArray() || items.isEmpty()) { - log.info("page {} 응답 없음. 중단.", page); - return; - } - - executor.submit(() -> { - for (JsonNode item : items) { - try { - FestivalItemDto dto = objectMapper.treeToValue(item, FestivalItemDto.class); - - if (LocalDate.parse(dto.getFstvlEndDate()).isBefore(LocalDate.now())) { - continue; - } - - if (!activityRepository.existsByName(dto.getName())) { - Activity activity = Activity.builder() - .name(dto.getName()) - .description(dto.getDescription()) - .streetAddress(dto.getStreetAddress()) - .lotNumberAddress(dto.getLotNumberAddress()) - .latitude(dto.getLatitude()) - .longitude(dto.getLongitude()) - .startDate(LocalDate.parse(dto.getFstvlStartDate())) - .endDate(LocalDate.parse(dto.getFstvlEndDate())) - .homepageUrl(dto.getHomepageUrl()) - .phoneNumber(dto.getPhoneNumber()) - .activityType(ActivityType.FESTIVAL) - .chosen(0L) - .shown(0L) - .CTR(0.0) - .build(); - activityRepository.save(activity); - - String vectorLiteral = embeddingClient.getEmbeddingLiteral(dto.getDescription()); - activityNativeRepository.updateVectorByName(dto.getName(), vectorLiteral); - - log.info("저장됨: " + dto.getName()); - } - - } catch (Exception e) { - log.error("항목 처리 중 오류 발생", e); - } - } - }); - page++; - Thread.sleep(100); // 속도 제한 - - } catch (Exception e) { - log.error("page {} 처리 중 오류 발생", page, e); - break; - } - } - } -} - diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java deleted file mode 100644 index a56982d..0000000 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/TourSpotApiService.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.example.silverbridgeX_user.activity.service; - -import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.activity.domain.ActivityType; -import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.TourSpotItemDto; -import com.example.silverbridgeX_user.activity.repository.ActivityNativeRepository; -import com.example.silverbridgeX_user.activity.repository.ActivityRepository; -import com.example.silverbridgeX_user.global.util.EmbeddingClient; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.LocalDate; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class TourSpotApiService { - - private final ActivityRepository activityRepository; - private final EmbeddingClient embeddingClient; - private final ActivityNativeRepository activityNativeRepository; - private final ActivityApiService activityApiService; - - private final ObjectMapper objectMapper = new ObjectMapper(); - String apiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_trrsrt_api"; - - public void fetchAndSaveTourSpotData() { - ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - - int page = 1; - while (true) { - try { - String urlStr = activityApiService.buildUrl(apiUrl, page); - String json = activityApiService.getJsonFromUrl(urlStr); - JsonNode items = activityApiService.parseItems(json); - - if (items == null || !items.isArray() || items.isEmpty()) { - log.info("page {} 응답 없음. 중단.", page); - return; - } - - executor.submit(() -> { - for (JsonNode item : items) { - try { - TourSpotItemDto dto = objectMapper.treeToValue(item, TourSpotItemDto.class); - - if (!activityRepository.existsByName(dto.getName())) { - Activity activity = Activity.builder() - .name(dto.getName()) - .description(dto.getDescription()) - .streetAddress(dto.getStreetAddress()) - .lotNumberAddress(dto.getLotNumberAddress()) - .latitude(dto.getLatitude()) - .longitude(dto.getLongitude()) - .startDate(LocalDate.parse(dto.getReferenceDate())) - .endDate(LocalDate.parse("2999-12-31")) - .phoneNumber(dto.getPhoneNumber()) - .activityType(ActivityType.TOUR_SPOT) - .chosen(0L) - .shown(0L) - .CTR(0.0) - .build(); - activityRepository.save(activity); - - String vectorLiteral = embeddingClient.getEmbeddingLiteral(dto.getDescription()); - activityNativeRepository.updateVectorByName(dto.getName(), vectorLiteral); - - log.info("저장됨: {}", dto.getName()); - } - } catch (Exception e) { - log.error("항목 처리 중 오류 발생", e); - } - } - }); - page++; - Thread.sleep(100); // 속도 제한 - - } catch (Exception e) { - log.error("page {} 처리 중 오류 발생", page, e); - break; - } - } - } - -} From f4691083360c9d518d011a3e90bf8be9daede593 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 17:25:23 +0900 Subject: [PATCH 12/41] =?UTF-8?q?fix:=20neo4j=EC=97=90=EC=84=9C=EC=9D=98?= =?UTF-8?q?=20=EC=9E=84=EB=B2=A0=EB=94=A9=20=EA=B0=92=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=9C=84=ED=95=B4=20type=20string=20->=20float=20[]?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/Activity.java | 2 -- .../activity/service/ActivityService.java | 24 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index 4202fdd..f039aa0 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -5,7 +5,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.Transient; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Builder; @@ -31,7 +30,6 @@ public class Activity { private String description; @Column(columnDefinition = "vector(384)") - @Transient private String descriptionEmbedding; private String streetAddress; diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java index 0839815..bb20b0a 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -257,6 +258,11 @@ public void insertActivitiesNeo4j() { List activities = activityRepository.findAll(); for (Activity activity : activities) { + List embeddingVector = parseEmbeddingVector(activity.getDescriptionEmbedding()); + String description = activity.getDescription().length() > 100 + ? activity.getDescription().substring(0, 100) + : activity.getDescription(); + try (Session session = driver.session()) { session.writeTransaction(tx -> { tx.run(""" @@ -272,8 +278,8 @@ public void insertActivitiesNeo4j() { Values.parameters( "id", activity.getId(), "name", activity.getName(), - "description", activity.getDescription(), - "embedding", activity.getDescriptionEmbedding(), + "description", description, + "embedding", embeddingVector, "latitude", activity.getLatitude(), "longitude", activity.getLongitude(), "startDate", activity.getStartDate(), @@ -285,4 +291,18 @@ public void insertActivitiesNeo4j() { } } + private List parseEmbeddingVector(String str) { + if (str == null || str.isBlank()) { + return List.of(); + } + + str = str.replaceAll("[\\[\\]]", ""); + String[] tokens = str.split(","); + List result = new ArrayList<>(); + for (String token : tokens) { + result.add(Float.parseFloat(token.trim())); + } + return result; + } + } From fc3720ac7eb9ac3d5884cb89c18e19833d784b34 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 17:36:25 +0900 Subject: [PATCH 13/41] =?UTF-8?q?refactor:=20JPA=EA=B0=80=20=EC=9E=84?= =?UTF-8?q?=EB=B2=A0=EB=94=A9=20=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=84=EC=9A=A9=20=ED=95=84=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=B7=A8=EA=B8=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/silverbridgeX_user/activity/domain/Activity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index f039aa0..9a85357 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -29,7 +29,7 @@ public class Activity { @Column(length = 1000) private String description; - @Column(columnDefinition = "vector(384)") + @Column(name = "description_embedding", columnDefinition = "vector(384)", insertable = false, updatable = false) private String descriptionEmbedding; private String streetAddress; From b742d446883fd757b71537b7805a0c5f2fbb5a2b Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 17:50:22 +0900 Subject: [PATCH 14/41] =?UTF-8?q?refactor:=20JPA=EA=B0=80=20=EC=9E=84?= =?UTF-8?q?=EB=B2=A0=EB=94=A9=20=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=84=EC=9A=A9=20=ED=95=84=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=B7=A8=EA=B8=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/silverbridgeX_user/user/domain/User.java | 7 ++----- .../silverbridgeX_user/user/service/UserService.java | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index 1fcd35b..f0384b0 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -7,7 +7,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; -import jakarta.persistence.Transient; import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -55,12 +54,10 @@ public class User extends BaseEntity { @JdbcTypeCode(SqlTypes.JSON) private List dislikeKeywords; - @Column(columnDefinition = "vector(384)") - @Transient + @Column(name = "preferred_embedding", columnDefinition = "vector(384)", insertable = false, updatable = false) private String preferredEmbedding; - @Column(columnDefinition = "vector(384)") - @Transient + @Column(name = "dislike_embedding", columnDefinition = "vector(384)", insertable = false, updatable = false) private String dislikeEmbedding; public User(String username, String nickname, String email) { diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index 3d87862..aa722d8 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -51,7 +51,7 @@ public User createUser(UserRequestDto.UserReqDto userReqDto) { userRepository.save(newUser); // Neo4j 사용자 노드 만들기 - //insertUserNodeIfNotExists(newUser.getId(), newUser.getUsername()); + insertUserNodeIfNotExists(newUser.getId(), newUser.getUsername()); manager.loadUserByUsername(userReqDto.getUsername()); // 저장된 사용자 정보를 다시 로드하여 동기화 시도 return newUser; @@ -162,9 +162,9 @@ public void insertUserNodeIfNotExists(Long userId, String name) { "keywords", List.of(), // 기본 null 대신 빈 리스트 "embedding", List.of() // 빈 float 배열 )); - System.out.println("사용자 노드 생성 완료: " + name); + log.info("사용자 노드 생성 완료: " + name); } else { - System.out.println("이미 존재: " + name); + log.info("이미 존재: " + name); } return null; }); From 3729e0c7587bf8a0fd9d274087ed873da32444db Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 17:55:49 +0900 Subject: [PATCH 15/41] =?UTF-8?q?refactor:=20username=20=3D=3D=20email?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/user/converter/UserConverter.java | 2 +- .../example/silverbridgeX_user/user/dto/UserRequestDto.java | 3 --- .../example/silverbridgeX_user/user/service/UserService.java | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java b/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java index 5940dd6..7c65aeb 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java @@ -11,7 +11,7 @@ public static User saveUser(UserRequestDto.UserReqDto userReqDto) { return User.builder() .email(userReqDto.getEmail()) - .username(userReqDto.getUsername()) + .username(userReqDto.getEmail()) .nickname(userReqDto.getNickname()) .build(); } diff --git a/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java b/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java index 79c9993..0a66c39 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java +++ b/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java @@ -19,9 +19,6 @@ public static class UserReqDto { @Schema(description = "이메일") private String email; - @Schema(description = "이메일id{social}") - private String username; - @Schema(description = "닉네임") private String nickname; diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index aa722d8..b92209b 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -53,7 +53,7 @@ public User createUser(UserRequestDto.UserReqDto userReqDto) { // Neo4j 사용자 노드 만들기 insertUserNodeIfNotExists(newUser.getId(), newUser.getUsername()); - manager.loadUserByUsername(userReqDto.getUsername()); // 저장된 사용자 정보를 다시 로드하여 동기화 시도 + manager.loadUserByUsername(userReqDto.getEmail()); // 저장된 사용자 정보를 다시 로드하여 동기화 시도 return newUser; } From 1a09088878d6a8a994a917874f0a192aa57a7974 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 21:23:27 +0900 Subject: [PATCH 16/41] =?UTF-8?q?refactor:=20driver=20=EC=84=A0=EC=96=B8?= =?UTF-8?q?=EB=B6=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/activity/service/ActivityService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java index bb20b0a..88d9b3f 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -22,6 +22,7 @@ import java.util.concurrent.Executors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Values; import org.springframework.stereotype.Service; @@ -35,7 +36,7 @@ public class ActivityService { private final ActivityNativeRepository activityNativeRepository; private final ActivityApiService activityApiService; - private final org.neo4j.driver.Driver driver; + private final Driver driver; private final ObjectMapper objectMapper = new ObjectMapper(); String festivalApiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_cltur_fstvl_api"; From 788d517f6abd963cee8c02a4f5712f2e475c8136 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 21:25:48 +0900 Subject: [PATCH 17/41] =?UTF-8?q?feature:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EB=B3=84=20=EC=B6=94=EC=B2=9C=20=ED=99=9C=EB=8F=99=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=EB=A1=9C=EC=A7=81,=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=9E=91=EC=84=B1=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecommendActivityController.java | 50 ++++++++++++------- .../converter/RecommendActivityConverter.java | 42 ++++++++++++++++ .../domain/RecommendActivity.java | 3 +- .../dto/RecommendActivityResponseDto.java | 45 +++++++++++++++++ .../RecommendActivityRepository.java | 4 +- .../service/RecommendActivityService.java | 27 ++++++---- .../global/api_payload/ErrorCode.java | 3 ++ .../global/api_payload/SuccessCode.java | 5 ++ 8 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java create mode 100644 src/main/java/com/example/silverbridgeX_user/RecommendActivity/dto/RecommendActivityResponseDto.java diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/controller/RecommendActivityController.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/controller/RecommendActivityController.java index eb3606c..93d5309 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/controller/RecommendActivityController.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/controller/RecommendActivityController.java @@ -1,5 +1,8 @@ package com.example.silverbridgeX_user.RecommendActivity.controller; +import com.example.silverbridgeX_user.RecommendActivity.converter.RecommendActivityConverter; +import com.example.silverbridgeX_user.RecommendActivity.domain.RecommendActivity; +import com.example.silverbridgeX_user.RecommendActivity.dto.RecommendActivityResponseDto.RecommendActivityResDtos; import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; import com.example.silverbridgeX_user.global.api_payload.ApiResponse; import com.example.silverbridgeX_user.global.api_payload.SuccessCode; @@ -9,10 +12,11 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -25,33 +29,45 @@ public class RecommendActivityController { private final RecommendActivityService recommendActivityService; private final UserService userService; - @Operation(summary = "활동 선택", description = "로그 저장, selected 간선 생성합니다.") + @Operation(summary = "활동 반환", description = "추천하는 활동 리스트를 반환합니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "ACTIVITY_2001", description = "선택한 활동 로그와 selected 간선 저장 완료했습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "ACTIVITY_2001", description = "추천하는 활동 리스를 반환 완료했습니다."), }) - @DeleteMapping("/select") - public ApiResponse select( - @RequestParam(name = "activityId") Long activityId, + @GetMapping("/recommend-activity") + public ApiResponse getRecommendActivities( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { User user = userService.findByUserName(customUserDetails.getUsername()); - recommendActivityService.handleActivitySelection(user, activityId); - return ApiResponse.onSuccess(SuccessCode.USER_LOGOUT_SUCCESS, "로그 저장 완료"); + List recommendActivities = recommendActivityService.getRecommendActivities(user); + return ApiResponse.onSuccess(SuccessCode.RECOMMEND_ACTIVITY_VIEW_LIST_SUCCESS, + RecommendActivityConverter.recommendActivityResDtos(recommendActivities)); } - @Operation(summary = "활동 열람", description = "로그 저장합니다.") + @Operation(summary = "활동 선택", description = "활동 선택 로그를 저장, selected 간선 생성합니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "ACTIVITY_2002", description = "열람한 활동 로그 저장 완료했습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "ACTIVITY_2002", description = "선택한 활동 로그와 selected 간선 저장 완료했습니다."), }) - @DeleteMapping("/view") - public ApiResponse view( - @RequestParam(name = "activityId") Long activityId, - @AuthenticationPrincipal CustomUserDetails customUserDetails + @PostMapping("/select") + public ApiResponse select( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestParam(name = "activityId") Long activityId ) { User user = userService.findByUserName(customUserDetails.getUsername()); - recommendActivityService.handleActivitySelection(user, activityId); + return ApiResponse.onSuccess(SuccessCode.RECOMMEND_ACTIVITY_SELECT_LOG_SUCCESS, "선택 로그 저장 완료"); + } - return ApiResponse.onSuccess(SuccessCode.USER_LOGOUT_SUCCESS, "로그 저장 완료"); + @Operation(summary = "활동 열람", description = "활동 열람 로그를 저장합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "ACTIVITY_2003", description = "열람한 활동 로그를 저장 완료했습니다."), + }) + @PostMapping("/view") + public ApiResponse view( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestParam(name = "activityId") Long activityId + ) { + User user = userService.findByUserName(customUserDetails.getUsername()); + recommendActivityService.handleActivityView(user, activityId); + return ApiResponse.onSuccess(SuccessCode.RECOMMEND_ACTIVITY_VIEW_LOG_SUCCESS, "열람 로그 저장 완료"); } } diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java new file mode 100644 index 0000000..603b698 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java @@ -0,0 +1,42 @@ +package com.example.silverbridgeX_user.RecommendActivity.converter; + +import com.example.silverbridgeX_user.RecommendActivity.domain.RecommendActivity; +import com.example.silverbridgeX_user.RecommendActivity.dto.RecommendActivityResponseDto.RecommendActivityResDto; +import com.example.silverbridgeX_user.RecommendActivity.dto.RecommendActivityResponseDto.RecommendActivityResDtos; +import com.example.silverbridgeX_user.activity.domain.Activity; +import java.util.List; +import java.util.stream.Collectors; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class RecommendActivityConverter { + public static RecommendActivityResDto recommendActivityResDto(RecommendActivity recommendActivity) { + Activity activity = recommendActivity.getActivity(); + String address = + activity.getStreetAddress().isEmpty() ? activity.getLotNumberAddress() : activity.getStreetAddress(); + String content = activity.getEndDate().equals("2999-12-31") + ? (activity.getHomepageUrl() != null && !activity.getHomepageUrl().isBlank() + ? activity.getHomepageUrl() + : activity.getPhoneNumber()) + : activity.getStartDate() + " ~ " + activity.getEndDate(); + + return RecommendActivityResDto.builder() + .tag(String.valueOf(activity.getActivityType())) + .name(activity.getName()) + .address(address) + .content(content) + .build(); + } + + public static RecommendActivityResDtos recommendActivityResDtos(List activities) { + + List dtoList = activities.stream() + .map(RecommendActivityConverter::recommendActivityResDto) + .collect(Collectors.toList()); + + return RecommendActivityResDtos.builder() + .recommendActivityResDtos(dtoList) + .build(); + } + +} diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/RecommendActivity.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/RecommendActivity.java index 72af850..dcb10c8 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/RecommendActivity.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/RecommendActivity.java @@ -1,5 +1,6 @@ package com.example.silverbridgeX_user.RecommendActivity.domain; +import com.example.silverbridgeX_user.activity.domain.Activity; import com.example.silverbridgeX_user.user.domain.User; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -34,6 +35,6 @@ public class RecommendActivity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "activity_id") - private User activity; + private Activity activity; } diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/dto/RecommendActivityResponseDto.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/dto/RecommendActivityResponseDto.java new file mode 100644 index 0000000..775d79b --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/dto/RecommendActivityResponseDto.java @@ -0,0 +1,45 @@ +package com.example.silverbridgeX_user.RecommendActivity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +public class RecommendActivityResponseDto { + + @Schema(description = "RecommendActivityResDtos") + @Getter + @Setter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class RecommendActivityResDtos { + @Schema(description = "추천 활동 리스트") + private List recommendActivityResDtos; + } + + @Schema(description = "RecommendActivityResDto") + @Getter + @Setter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class RecommendActivityResDto { + @Schema(description = "태그") + private String tag; + + @Schema(description = "이름") + private String name; + + @Schema(description = "주소") + private String address; + + @Schema(description = "기간/링크/전화번호") + private String content; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java index 14877d0..b97ab58 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java @@ -1,8 +1,10 @@ package com.example.silverbridgeX_user.RecommendActivity.repository; import com.example.silverbridgeX_user.RecommendActivity.domain.RecommendActivity; +import com.example.silverbridgeX_user.user.domain.User; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface RecommendActivityRepository extends JpaRepository { - + List findAllByUser(User user); } diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java index 7956a04..8365b0e 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java @@ -1,5 +1,7 @@ package com.example.silverbridgeX_user.RecommendActivity.service; +import com.example.silverbridgeX_user.RecommendActivity.domain.RecommendActivity; +import com.example.silverbridgeX_user.RecommendActivity.repository.RecommendActivityRepository; import com.example.silverbridgeX_user.activity.converter.ActivityLogConverter; import com.example.silverbridgeX_user.activity.domain.Activity; import com.example.silverbridgeX_user.activity.domain.ActivityLog; @@ -9,6 +11,7 @@ import com.example.silverbridgeX_user.global.exception.GeneralException; import com.example.silverbridgeX_user.user.domain.User; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,9 +25,14 @@ public class RecommendActivityService { private final ActivityLogRepository activityLogRepository; private final ActivityRepository activityRepository; - private final Driver neo4jDriver; // Neo4j Java Driver + private final RecommendActivityRepository recommendActivityRepository; + private final Driver neo4jDriver; - public void handleActivitySelection(User user, Long activityId){ + public List getRecommendActivities(User user) { + return recommendActivityRepository.findAllByUser(user); + } + + public void handleActivitySelection(User user, Long activityId) { LocalDateTime now = LocalDateTime.now(); // 1. 로그 저장 Activity activity = activityRepository.findById(activityId) @@ -37,16 +45,16 @@ public void handleActivitySelection(User user, Long activityId){ session.writeTransaction(tx -> { // 기존 :PREFERRED 제거 tx.run(""" - MATCH (u:User {id: $uid})-[r:PREFERRED]->(a:Activity {id: $aid}) - DELETE r - """, + MATCH (u:User {id: $uid})-[r:PREFERRED]->(a:Activity {id: $aid}) + DELETE r + """, Map.of("uid", user.getId().toString(), "aid", "act" + activity.getId())); // :SELECTED 생성 tx.run(""" - MATCH (u:User {id: $uid}), (a:Activity {id: $aid}) - MERGE (u)-[:SELECTED]->(a) - """, + MATCH (u:User {id: $uid}), (a:Activity {id: $aid}) + MERGE (u)-[:SELECTED]->(a) + """, Map.of("uid", user.getId().toString(), "aid", "act" + activity.getId())); return null; @@ -54,7 +62,8 @@ public void handleActivitySelection(User user, Long activityId){ } } - public void handleActivityView(User user, Long activityId, LocalDateTime now){ + public void handleActivityView(User user, Long activityId) { + LocalDateTime now = LocalDateTime.now(); Activity activity = activityRepository.findById(activityId) .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); ActivityLog log = ActivityLogConverter.saveActivityLog(user, activity, now, "VIEW"); diff --git a/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java b/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java index 4a20715..d97b30b 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java +++ b/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java @@ -31,6 +31,9 @@ public enum ErrorCode implements BaseCode { // Activity ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "ACTIVITY_4041", "존재하지 않는 활동입니다."), + // Recommend Activity + RECOMMEND_ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "RECOMMEND_4041", "존재하지 않는 추천입니다."), + ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java b/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java index 58ea7ce..bbb68a5 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java +++ b/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java @@ -23,6 +23,11 @@ public enum SuccessCode implements BaseCode { USER_PROFILE_IMAGE_UPDATE_SUCCESS(HttpStatus.OK, "USER_2007", "프로필 사진 이미지 업로드가 완료 되었습니다."), USER_MYPAGE_VIEW_SUCCESS(HttpStatus.OK, "USER_2010", "마이페이지 정보 조회가 완료되었습니다."), + // Recommend Activity + RECOMMEND_ACTIVITY_VIEW_LIST_SUCCESS(HttpStatus.OK, "ACTIVITY_2001", "추천하는 활동 리스를 반환 완료했습니다."), + RECOMMEND_ACTIVITY_SELECT_LOG_SUCCESS(HttpStatus.OK, "ACTIVITY_2002", "선택한 활동 로그와 selected 간선 저장 완료했습니다."), + RECOMMEND_ACTIVITY_VIEW_LOG_SUCCESS(HttpStatus.OK, "ACTIVITY_2002", "열람한 활동 로그를 저장 완료했습니다."), + ; private final HttpStatus httpStatus; From adb69c8788a394bd959839d80d992e23c8925b25 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 21:26:26 +0900 Subject: [PATCH 18/41] refactor: username == email --- .../silverbridgeX_user/user/controller/TokenController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java b/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java index 5460bb7..4247f16 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java +++ b/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java @@ -41,16 +41,16 @@ public ApiResponse tokenToFront( String signIn = "wasUser"; if (isMember) { - User user = userService.findByUserName(userReqDto.getUsername()); + User user = userService.findByUserName(userReqDto.getEmail()); - JwtDto jwt = userService.jwtMakeSave(userReqDto.getUsername()); + JwtDto jwt = userService.jwtMakeSave(userReqDto.getEmail()); accessToken = jwt.getAccessToken(); refreshToken = jwt.getRefreshToken(); } else { User newUser = userService.createUser(userReqDto); - JwtDto jwt = userService.jwtMakeSave(userReqDto.getUsername()); + JwtDto jwt = userService.jwtMakeSave(userReqDto.getEmail()); accessToken = jwt.getAccessToken(); refreshToken = jwt.getRefreshToken(); From a84351bd31859e69d97bc94a369e2474012497bb Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 21:57:42 +0900 Subject: [PATCH 19/41] =?UTF-8?q?refactor:=20user=20id=EB=A5=BC=20string?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=8C=20long=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=83=90=EC=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RecommendActivityService.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java index 8365b0e..cd822ae 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java @@ -37,8 +37,8 @@ public void handleActivitySelection(User user, Long activityId) { // 1. 로그 저장 Activity activity = activityRepository.findById(activityId) .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); - ActivityLog log = ActivityLogConverter.saveActivityLog(user, activity, now, "SELECT"); - activityLogRepository.save(log); + ActivityLog activityLog = ActivityLogConverter.saveActivityLog(user, activity, now, "SELECT"); + activityLogRepository.save(activityLog); // 2. 간선 갱신 try (Session session = neo4jDriver.session()) { @@ -48,14 +48,26 @@ public void handleActivitySelection(User user, Long activityId) { MATCH (u:User {id: $uid})-[r:PREFERRED]->(a:Activity {id: $aid}) DELETE r """, - Map.of("uid", user.getId().toString(), "aid", "act" + activity.getId())); + Map.of("uid", user.getId(), "aid", activity.getId())); // :SELECTED 생성 tx.run(""" MATCH (u:User {id: $uid}), (a:Activity {id: $aid}) MERGE (u)-[:SELECTED]->(a) """, - Map.of("uid", user.getId().toString(), "aid", "act" + activity.getId())); + Map.of("uid", user.getId(), "aid", activity.getId())); + + // 생성됐는지 확인 + var checkResult = tx.run(""" + MATCH (u:User {id: $uid})-[r:SELECTED]->(a:Activity {id: $aid}) + RETURN COUNT(r) > 0 AS exists + """, + Map.of("uid", user.getId(), "aid", activity.getId())); + + boolean exists = checkResult.single().get("exists").asBoolean(); + if (!exists) { + log.error("사용자 활동 SELECT 간선이 생기지 않았습니다."); + } return null; }); From ef5e353970073a023de3808742abf86512d92d37 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 22:30:42 +0900 Subject: [PATCH 20/41] =?UTF-8?q?refactor:=20user=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20id=EB=A7=8C=20=EC=9A=B0=EC=84=A0?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UserService.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index b92209b..fb6482e 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -12,7 +12,6 @@ import com.example.silverbridgeX_user.user.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; -import java.util.List; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -51,7 +50,7 @@ public User createUser(UserRequestDto.UserReqDto userReqDto) { userRepository.save(newUser); // Neo4j 사용자 노드 만들기 - insertUserNodeIfNotExists(newUser.getId(), newUser.getUsername()); + insertUserNodeIfNotExists(newUser.getId()); manager.loadUserByUsername(userReqDto.getEmail()); // 저장된 사용자 정보를 다시 로드하여 동기화 시도 return newUser; @@ -138,7 +137,7 @@ public void deleteUser(String username) { log.info("{} 회원 탈퇴 완료", username); } - public void insertUserNodeIfNotExists(Long userId, String name) { + public void insertUserNodeIfNotExists(Long userId) { try (Session session = driver.session()) { session.writeTransaction(tx -> { // 존재 여부 확인 @@ -153,18 +152,13 @@ public void insertUserNodeIfNotExists(Long userId, String name) { CREATE (u:User { id: $id, name: $name, - keywords: $keywords, - embedding: $embedding }) """, Map.of( - "id", userId, - "name", name, - "keywords", List.of(), // 기본 null 대신 빈 리스트 - "embedding", List.of() // 빈 float 배열 + "id", userId )); - log.info("사용자 노드 생성 완료: " + name); + log.info("사용자 노드 생성 완료: " + userId); } else { - log.info("이미 존재: " + name); + log.info("이미 존재: " + userId); } return null; }); From 95120e5277c5a8596bc094f89ff12cbf11f0e7c0 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 22:31:50 +0900 Subject: [PATCH 21/41] =?UTF-8?q?refactor:=20=ED=81=B4=EB=A6=AD=EC=88=98?= =?UTF-8?q?=EC=99=80=20=EB=85=B8=EC=B6=9C=EC=88=98=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BB=AC=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/silverbridgeX_user/activity/domain/Activity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index 9a85357..99cea98 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -48,9 +48,9 @@ public class Activity { private String phoneNumber; - private Long chosen; + private Long clickNum; - private Long shown; + private Long impressionNum; private Double CTR; From b3fd67da53eac80b2efc872a4c444eb36803f3dc Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 23:01:05 +0900 Subject: [PATCH 22/41] =?UTF-8?q?refactor:=20=ED=81=B4=EB=A6=AD=EC=88=98?= =?UTF-8?q?=EC=99=80=20=EB=85=B8=EC=B6=9C=EC=88=98=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BB=AC=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/activity/domain/Activity.java | 2 +- .../activity/service/ActivityService.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index 99cea98..9fb0f13 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -52,7 +52,7 @@ public class Activity { private Long impressionNum; - private Double CTR; + private Double ctr; public void updateCoordinate(String longitude, String latitude) { this.longitude = longitude; diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java index 88d9b3f..ddc33ee 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -80,9 +80,9 @@ public void fetchAndSaveFestivalData() { .homepageUrl(dto.getHomepageUrl()) .phoneNumber(dto.getPhoneNumber()) .activityType(ActivityType.FESTIVAL) - .chosen(0L) - .shown(0L) - .CTR(0.0) + .clickNum(0L) + .impressionNum(0L) + .ctr(0.0) .build(); activityRepository.save(activity); @@ -139,9 +139,9 @@ public void fetchAndSaveTourSpotData() { .endDate(LocalDate.parse("2999-12-31")) .phoneNumber(dto.getPhoneNumber()) .activityType(ActivityType.TOUR_SPOT) - .chosen(0L) - .shown(0L) - .CTR(0.0) + .clickNum(0L) + .impressionNum(0L) + .ctr(0.0) .build(); activityRepository.save(activity); From b2e439f583e418ae132dd3cd3e18d99f3b104b94 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 29 May 2025 23:02:44 +0900 Subject: [PATCH 23/41] =?UTF-8?q?core:=20=ED=99=9C=EB=8F=99=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=A0=80=EC=9E=A5=20=EC=9C=84=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/ActivityRanking.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java new file mode 100644 index 0000000..e26283d --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java @@ -0,0 +1,33 @@ +package com.example.silverbridgeX_user.activity.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ActivityRanking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "activity_id") + private Activity activity; + + private Integer rank; + + private LocalDate createdAt; +} From 06cd49d5f2d3c8cf897822fd18fa4444d656b72e Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 30 May 2025 14:16:32 +0900 Subject: [PATCH 24/41] =?UTF-8?q?refactor:=20user=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=EC=97=90=20id=EB=A7=8C=20=EC=86=8D=EC=84=B1=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B0=80=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/silverbridgeX_user/user/service/UserService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index fb6482e..4c44305 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -150,8 +150,7 @@ public void insertUserNodeIfNotExists(Long userId) { if (!exists) { tx.run(""" CREATE (u:User { - id: $id, - name: $name, + id: $id }) """, Map.of( "id", userId From 16f657898ef39346667af7d3f46380af816faad1 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 5 Jun 2025 20:42:49 +0900 Subject: [PATCH 25/41] =?UTF-8?q?refactor:=20@NoArgsConstructor=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EC=97=86=EA=B8=B0=EC=97=90=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/converter/ActivityLogConverter.java | 7 ++----- .../silverbridgeX_user/user/converter/UserConverter.java | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java b/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java index 0cd2ab5..ab793ff 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java @@ -4,18 +4,15 @@ import com.example.silverbridgeX_user.activity.domain.Activity; import com.example.silverbridgeX_user.activity.domain.ActivityLog; import com.example.silverbridgeX_user.user.domain.User; -import com.example.silverbridgeX_user.user.dto.UserRequestDto; import java.time.LocalDateTime; import java.util.Objects; -import lombok.NoArgsConstructor; -@NoArgsConstructor public class ActivityLogConverter { public static ActivityLog saveActivityLog(User user, Activity activity, LocalDateTime now, String type) { ActionType actionType = null; - if(Objects.equals(type, "View")){ + if (Objects.equals(type, "View")) { actionType = ActionType.VIEW; - }else if(Objects.equals(type, "SELECT")){ + } else if (Objects.equals(type, "SELECT")) { actionType = ActionType.SELECT; } diff --git a/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java b/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java index 7c65aeb..9a050b9 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java @@ -3,9 +3,7 @@ import com.example.silverbridgeX_user.user.domain.User; import com.example.silverbridgeX_user.user.dto.JwtDto; import com.example.silverbridgeX_user.user.dto.UserRequestDto; -import lombok.NoArgsConstructor; -@NoArgsConstructor public class UserConverter { public static User saveUser(UserRequestDto.UserReqDto userReqDto) { From ad144e0e2ee22f05577f6b58776c32a9fa2663a7 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:15:41 +0900 Subject: [PATCH 26/41] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EC=97=90=20Spring=20Batch=20=EB=B0=8F=20Sche?= =?UTF-8?q?duling=20=EA=B8=B0=EB=8A=A5=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SilverbridgeXUserApplication.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/SilverbridgeXUserApplication.java b/src/main/java/com/example/silverbridgeX_user/SilverbridgeXUserApplication.java index b3b6701..23fdf6a 100644 --- a/src/main/java/com/example/silverbridgeX_user/SilverbridgeXUserApplication.java +++ b/src/main/java/com/example/silverbridgeX_user/SilverbridgeXUserApplication.java @@ -1,13 +1,18 @@ package com.example.silverbridgeX_user; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + @SpringBootApplication +@EnableBatchProcessing +@EnableScheduling public class SilverbridgeXUserApplication { - public static void main(String[] args) { - SpringApplication.run(SilverbridgeXUserApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SilverbridgeXUserApplication.class, args); + } } From f0cf450a4d60c413dc5a99f8b83dd3ed91052258 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:16:50 +0900 Subject: [PATCH 27/41] =?UTF-8?q?feat:=20=EC=84=A0=ED=98=B8=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20api=EB=A1=9C=20=EB=B6=88=EB=9F=AC=EC=99=80?= =?UTF-8?q?=EC=84=9C=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UserService.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index 4c44305..0f081fc 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -7,19 +7,23 @@ import com.example.silverbridgeX_user.user.domain.User; import com.example.silverbridgeX_user.user.dto.JwtDto; import com.example.silverbridgeX_user.user.dto.UserRequestDto; +import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserPreferenceDto; import com.example.silverbridgeX_user.user.jwt.JwtTokenUtils; import com.example.silverbridgeX_user.user.repository.RefreshTokenRepository; import com.example.silverbridgeX_user.user.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; +import java.util.List; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; @Slf4j @Service @@ -29,9 +33,11 @@ public class UserService { private final RefreshTokenRepository refreshTokenRepository; private final JpaUserDetailsManager manager; private final JwtTokenUtils jwtTokenUtils; - + private final RestTemplate restTemplate; private final Driver driver; + private final String PREF_URL = "http://localhost:3000/summary/preferences/all"; + @Transactional public User findByUserName(String userName) { return userRepository.findByUsername(userName) @@ -174,4 +180,25 @@ public void saveNickname(UserRequestDto.UserNicknameReqDto nicknameReqDto, User user.updateNickname(nickname); } + public void updatePreferredKeywords() { + ResponseEntity response + = restTemplate.getForEntity(PREF_URL, UserPreferenceDto[].class); + UserPreferenceDto[] dtos = response.getBody(); + + if (dtos == null) { + return; + } + + for (UserPreferenceDto dto : dtos) { + userRepository.findById(dto.getUserId()).ifPresent(user -> { + List deduplicated = dto.getPreferences().stream() + .distinct() + .toList(); + + user.updatePreferredKeywords(deduplicated); + userRepository.save(user); + log.info(user.toString()); + }); + } + } } From cea1c72ee72b6a5a6365877e1911d0331430719c Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:17:14 +0900 Subject: [PATCH 28/41] =?UTF-8?q?feat:=20=EC=84=A0=ED=98=B8=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20api=EB=A1=9C=20=EB=B6=88=EB=9F=AC=EC=99=80?= =?UTF-8?q?=EC=84=9C=20=EC=A0=80=EC=9E=A5=EC=9C=84=ED=95=9C=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/user/dto/UserRequestDto.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java b/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java index 0a66c39..f5224a4 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java +++ b/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java @@ -1,6 +1,7 @@ package com.example.silverbridgeX_user.user.dto; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -55,4 +56,16 @@ public static class UserAddressReqDto { @Schema(description = "상세 주소") private String detailedAddress; } + + @Schema(description = "UserPreferenceDto") + @Getter + @Setter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class UserPreferenceDto { + private Long userId; + private List preferences; + } + } From 73ba6488ba2b5001d7823aeef59250d429026135 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:22:18 +0900 Subject: [PATCH 29/41] =?UTF-8?q?feat:=20=EC=B6=94=EC=B2=9C=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EB=A1=9C=EC=A7=81=EC=9D=84=20Spring=20Bat?= =?UTF-8?q?ch=20+=20Scheduler=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=83=88=EB=B2=BD=203=EC=8B=9C=EC=97=90=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EB=B3=84=20=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +- .../converter/RecommendActivityConverter.java | 14 +- .../RecommendActivityRepository.java | 2 + .../service/RecommendActivityService.java | 139 ++++++++++++++++++ .../activity/dto/ActivityResponseDto.java | 2 - .../repository/ActivityRankingRepository.java | 28 ++++ .../repository/ActivityRepository.java | 16 ++ .../activity/service/ActivityService.java | 10 +- .../batch/job/RecommendationJobConfig.java | 45 ++++++ .../batch/scheduler/BatchScheduler.java | 29 ++++ .../batch/step/CalculateCtrConfig.java | 21 +++ .../batch/step/GenRecommendationConfig.java | 20 +++ .../batch/step/PruneEdgeConfig.java | 20 +++ .../batch/step/RequestActEmbConfig.java | 21 +++ .../batch/step/RequestUserEmbConfig.java | 21 +++ .../batch/step/SaveTopRankConfig.java | 20 +++ .../batch/step/UpdateGraphConfig.java | 20 +++ .../batch/tasklet/CalculateCtrTasklet.java | 40 +++++ .../tasklet/GenRecommendationTasklet.java | 22 +++ .../batch/tasklet/PruneEdgeTasklet.java | 27 ++++ .../batch/tasklet/RequestActEmbTasklet.java | 27 ++++ .../batch/tasklet/RequestUserEmbTasklet.java | 55 +++++++ .../batch/tasklet/SaveTopRankTasklet.java | 36 +++++ .../batch/tasklet/UpdateGraphTasklet.java | 97 ++++++++++++ .../global/config/RestTemplateConfig.java | 13 ++ .../global/config/SchedulerConfig.java | 15 ++ .../global/util/EmbeddingClient.java | 39 ++++- .../silverbridgeX_user/user/domain/User.java | 6 + .../user/repository/UserRepository.java | 30 ++++ 29 files changed, 833 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRankingRepository.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/job/RecommendationJobConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/scheduler/BatchScheduler.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/CalculateCtrConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/GenRecommendationConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/PruneEdgeConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/RequestActEmbConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/RequestUserEmbConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/SaveTopRankConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/step/UpdateGraphConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java create mode 100644 src/main/java/com/example/silverbridgeX_user/global/config/RestTemplateConfig.java create mode 100644 src/main/java/com/example/silverbridgeX_user/global/config/SchedulerConfig.java diff --git a/build.gradle b/build.gradle index f021f8d..a92f41a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,9 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' implementation 'org.springframework.boot:spring-boot-starter-security' + // batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -44,9 +47,9 @@ dependencies { // db implementation 'org.postgresql:postgresql:42.7.1' -// implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.neo4j.driver:neo4j-java-driver:5.14.0' - +// implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // test testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java index 603b698..5106382 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/converter/RecommendActivityConverter.java @@ -4,12 +4,22 @@ import com.example.silverbridgeX_user.RecommendActivity.dto.RecommendActivityResponseDto.RecommendActivityResDto; import com.example.silverbridgeX_user.RecommendActivity.dto.RecommendActivityResponseDto.RecommendActivityResDtos; import com.example.silverbridgeX_user.activity.domain.Activity; +import com.example.silverbridgeX_user.user.domain.User; +import java.time.LocalDate; import java.util.List; import java.util.stream.Collectors; -import lombok.NoArgsConstructor; -@NoArgsConstructor public class RecommendActivityConverter { + public static RecommendActivity saveRecommendActivity(LocalDate date, Integer number, User user, + Activity activity) { + return RecommendActivity.builder() + .date(date) + .user(user) + .number(number) + .activity(activity) + .build(); + } + public static RecommendActivityResDto recommendActivityResDto(RecommendActivity recommendActivity) { Activity activity = recommendActivity.getActivity(); String address = diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java index b97ab58..a88f2b9 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/repository/RecommendActivityRepository.java @@ -7,4 +7,6 @@ public interface RecommendActivityRepository extends JpaRepository { List findAllByUser(User user); + + void deleteByUser(User user); } diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java index cd822ae..5b6592e 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java @@ -1,16 +1,20 @@ package com.example.silverbridgeX_user.RecommendActivity.service; +import com.example.silverbridgeX_user.RecommendActivity.converter.RecommendActivityConverter; import com.example.silverbridgeX_user.RecommendActivity.domain.RecommendActivity; import com.example.silverbridgeX_user.RecommendActivity.repository.RecommendActivityRepository; import com.example.silverbridgeX_user.activity.converter.ActivityLogConverter; import com.example.silverbridgeX_user.activity.domain.Activity; import com.example.silverbridgeX_user.activity.domain.ActivityLog; import com.example.silverbridgeX_user.activity.repository.ActivityLogRepository; +import com.example.silverbridgeX_user.activity.repository.ActivityRankingRepository; import com.example.silverbridgeX_user.activity.repository.ActivityRepository; import com.example.silverbridgeX_user.global.api_payload.ErrorCode; import com.example.silverbridgeX_user.global.exception.GeneralException; import com.example.silverbridgeX_user.user.domain.User; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -26,6 +30,7 @@ public class RecommendActivityService { private final ActivityLogRepository activityLogRepository; private final ActivityRepository activityRepository; private final RecommendActivityRepository recommendActivityRepository; + private final ActivityRankingRepository activityRankingRepository; private final Driver neo4jDriver; public List getRecommendActivities(User user) { @@ -81,4 +86,138 @@ public void handleActivityView(User user, Long activityId) { ActivityLog log = ActivityLogConverter.saveActivityLog(user, activity, now, "VIEW"); activityLogRepository.save(log); } + + public void execWrite(String cypher) { + try (Session s = neo4jDriver.session()) { + s.writeTransaction(tx -> { + tx.run(cypher); + return null; + }); + } + } + + /** + * :PREFERRED 간선 베치 삽입 + */ + public void insertPreferredEdges(List> rows) { + try (var ses = neo4jDriver.session()) { + ses.writeTransaction(tx -> { + for (var r : rows) { + Long userId = ((Number) r.get("user_id")).longValue(); + Long activityId = ((Number) r.get("activity_id")).longValue(); + Double score = ((Number) r.get("score")).doubleValue(); + + tx.run( + "MATCH (u:User {id: $uid}), (a:Activity {id: $aid}) " + + "MERGE (u)-[:PREFERRED {score: $score}]->(a)", + Map.of( + "uid", userId, + "aid", activityId, + "score", score + ) + ); + } + return null; + }); + } + } + + + /** + * :SIMILAR 간선 베치 삽입 + */ + public void insertSimilarEdges(List> rows) { + try (var ses = neo4jDriver.session()) { + ses.writeTransaction(tx -> { + for (var r : rows) { + Long uid1 = ((Number) r.get("uid1")).longValue(); + Long uid2 = ((Number) r.get("uid2")).longValue(); + Double wSim = ((Number) r.get("w_sim")).doubleValue(); + + tx.run(""" + MATCH (u1:User {id: $uid1}), (u2:User {id: $uid2}) + MERGE (u1)-[rel:SIMILAR]-(u2) + SET rel.score = $w_sim + """, Map.of( + "uid1", uid1, + "uid2", uid2, + "w_sim", wSim + )); + } + return null; + }); + } + } + + /** + * 하루에 한 번 실행 – prefer 14 + 인기 4 + 랜덤 2 + */ + public void generateAllUsersRecommendation() { + + // 0) 모든 사용자 조회 (PostgreSQL) + List users = activityRepository.findAllUsers(); // → 필요 메서드 구현 + + try (Session session = neo4jDriver.session()) { + + for (User user : users) { + + /* ---------- 1. PREFERRED 상위 10 & SIMILAR 사용자 SELECT 상위 N ---------- */ + List preferredIds = session.readTransaction(tx -> tx.run(""" + MATCH (u:User {id:$uid})-[r:PREFERRED]->(a:Activity) + RETURN a.id AS aid + ORDER BY r.score DESC + LIMIT 10 + """, Map.of("uid", user.getId())) + .list(r -> r.get("aid").asLong())); + + // SIMILAR 사용자들의 SELECT 상위(클릭 많은) 활동 + List similarTopIds = session.readTransaction(tx -> tx.run(""" + MATCH (u:User {id:$uid})-[:SIMILAR]-(s:User)-[:SELECTED]->(a:Activity) + WITH a, count(*) AS freq + ORDER BY freq DESC + RETURN a.id AS aid + LIMIT 14 + """, Map.of("uid", user.getId())) + .list(r -> r.get("aid").asLong())); + + LinkedHashSet resultIds = new LinkedHashSet<>(); + resultIds.addAll(preferredIds); + for (Long id : similarTopIds) { + if (resultIds.size() >= 14) { + break; + } + resultIds.add(id); + } + + /* ---------- 2. 인기 랭킹 + 거리 필터 ---------- */ + List popularIds = activityRankingRepository.fetchTopByDistance( + Float.parseFloat(user.getLatitude()), Float.parseFloat(user.getLongitude()), 18, resultIds); + + for (Long id : popularIds) { + if (resultIds.size() >= 18) { + break; + } + resultIds.add(id); + } + + /* ---------- 3. 무작위 2 ---------- */ + List randomIds = activityRepository.pickRandom(2, resultIds); + resultIds.addAll(randomIds); + + + /* ---------- 4. 추천 리스트 저장 ---------- */ + recommendActivityRepository.deleteByUser(user); // 기존 추천 삭제 + + int order = 0; + for (Long aid : resultIds) { + LocalDate today = LocalDate.now(); + Activity activity = activityRepository.findById(aid) + .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); + RecommendActivity recommendActivity + = RecommendActivityConverter.saveRecommendActivity(today, order++, user, activity); + recommendActivityRepository.save(recommendActivity); + } + } + } + } } diff --git a/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java b/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java index d540ca5..71eb107 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/dto/ActivityResponseDto.java @@ -7,9 +7,7 @@ import lombok.Setter; import lombok.ToString; -@Getter @NoArgsConstructor -@ToString public class ActivityResponseDto { @Getter diff --git a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRankingRepository.java b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRankingRepository.java new file mode 100644 index 0000000..4fae27c --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRankingRepository.java @@ -0,0 +1,28 @@ +package com.example.silverbridgeX_user.activity.repository; + +import com.example.silverbridgeX_user.activity.domain.ActivityRanking; +import java.util.Collection; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ActivityRankingRepository extends JpaRepository { + /** + * 사용자 위치로부터 10 km 이내 인기 rank 상위 N 반환 + */ + @Query(value = """ + SELECT ar.activity_id + FROM activity_ranking ar + JOIN activity a ON a.id = ar.activity_id + WHERE earth_distance( + ll_to_earth(CAST(:lat AS float), CAST(:lon AS float)), + ll_to_earth(CAST(a.latitude AS float), CAST(a.longitude AS float)) + ) < 10000 + AND ar.activity_id NOT IN (:excluded) + ORDER BY ar.rank ASC + LIMIT :limit + """, nativeQuery = true) + List fetchTopByDistance(double lat, double lon, + int limit, Collection excluded); + +} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java index 76320f5..ff97c2e 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java @@ -1,8 +1,24 @@ package com.example.silverbridgeX_user.activity.repository; import com.example.silverbridgeX_user.activity.domain.Activity; +import com.example.silverbridgeX_user.user.domain.User; +import java.util.Collection; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface ActivityRepository extends JpaRepository { boolean existsByName(String name); // 중복 방지 + + @Query(value = """ + SELECT id FROM activity + WHERE id NOT IN (:excluded) + ORDER BY random() + LIMIT :limit + """, nativeQuery = true) + List pickRandom(int limit, Collection excluded); + + @Query("select u from User u") + List findAllUsers(); + } diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java index ddc33ee..590892a 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -9,8 +9,10 @@ import com.example.silverbridgeX_user.global.api_payload.ErrorCode; import com.example.silverbridgeX_user.global.exception.GeneralException; import com.example.silverbridgeX_user.global.util.EmbeddingClient; +import com.example.silverbridgeX_user.user.repository.UserRepository; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.transaction.Transactional; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -35,6 +37,7 @@ public class ActivityService { private final EmbeddingClient embeddingClient; private final ActivityNativeRepository activityNativeRepository; private final ActivityApiService activityApiService; + private final UserRepository userRepository; private final Driver driver; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -270,7 +273,6 @@ public void insertActivitiesNeo4j() { MERGE (a:Activity {id: $id}) SET a.name = $name, a.description = $description, - a.embedding = $embedding, a.latitude = $latitude, a.longitude = $longitude, a.startDate = $startDate, @@ -280,7 +282,6 @@ public void insertActivitiesNeo4j() { "id", activity.getId(), "name", activity.getName(), "description", description, - "embedding", embeddingVector, "latitude", activity.getLatitude(), "longitude", activity.getLongitude(), "startDate", activity.getStartDate(), @@ -306,4 +307,9 @@ private List parseEmbeddingVector(String str) { return result; } + @Transactional + public void updateActivityEmbedding() { + System.out.println("startttt"); + System.out.println(userRepository.updateActivityEmbeddingAvgForUsers()); + } } diff --git a/src/main/java/com/example/silverbridgeX_user/batch/job/RecommendationJobConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/job/RecommendationJobConfig.java new file mode 100644 index 0000000..1d252c1 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/job/RecommendationJobConfig.java @@ -0,0 +1,45 @@ +package com.example.silverbridgeX_user.batch.job; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@EnableBatchProcessing +public class RecommendationJobConfig { + + private final JobRepository jobRepository; + + /* 각 Step Bean 주입 */ + private final Step step1_CTR; + private final Step step2_Rank; + private final Step step3_UserEmb; + private final Step step4_ActEmb; + private final Step step5_Graph; + private final Step step6_Prune; + private final Step step7_FinalRec; + + /** + * 매일 새벽 3시 실행 (Scheduler 에서 호출) + */ + @Bean + public Job recommendationJob() { + return (Job) new JobBuilder("recommendationJob", jobRepository) + .start(step1_CTR) + .next(step2_Rank) + .next(step3_UserEmb) + .next(step4_ActEmb) + .next(step5_Graph) + .next(step6_Prune) + .next(step7_FinalRec) + .build(); + } +} + + diff --git a/src/main/java/com/example/silverbridgeX_user/batch/scheduler/BatchScheduler.java b/src/main/java/com/example/silverbridgeX_user/batch/scheduler/BatchScheduler.java new file mode 100644 index 0000000..026cd20 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/scheduler/BatchScheduler.java @@ -0,0 +1,29 @@ +package com.example.silverbridgeX_user.batch.scheduler; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BatchScheduler { + + private final JobLauncher jobLauncher; + private final Job recommendationJob; + + /** + * 매일 새벽 3시(Asia/Seoul) 실행 + */ + @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") + public void runRecommendationJob() throws Exception { + JobParameters params = new JobParametersBuilder() + .addLong("run.id", System.currentTimeMillis()) // 파라미터 key는 중복 안 되게 + .toJobParameters(); + + jobLauncher.run(recommendationJob, params); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/CalculateCtrConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/CalculateCtrConfig.java new file mode 100644 index 0000000..fe23947 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/CalculateCtrConfig.java @@ -0,0 +1,21 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.CalculateCtrTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class CalculateCtrConfig { + @Bean + public Step step1_CTR(JobRepository jobRepository, + PlatformTransactionManager txManager, + CalculateCtrTasklet tasklet) { + return new StepBuilder("step1_CTR", jobRepository) + .tasklet(tasklet, txManager) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/GenRecommendationConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/GenRecommendationConfig.java new file mode 100644 index 0000000..84cd54c --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/GenRecommendationConfig.java @@ -0,0 +1,20 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.GenRecommendationTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class GenRecommendationConfig { + @Bean + public Step step7_FinalRec(JobRepository repo, PlatformTransactionManager tx, + GenRecommendationTasklet tasklet) { + return new StepBuilder("step7_FinalRec", repo) + .tasklet(tasklet, tx) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/PruneEdgeConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/PruneEdgeConfig.java new file mode 100644 index 0000000..718f17d --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/PruneEdgeConfig.java @@ -0,0 +1,20 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.PruneEdgeTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class PruneEdgeConfig { + @Bean + public Step step6_Prune(JobRepository repo, PlatformTransactionManager tx, + PruneEdgeTasklet tasklet) { + return new StepBuilder("step6_Prune", repo) + .tasklet(tasklet, tx) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/RequestActEmbConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/RequestActEmbConfig.java new file mode 100644 index 0000000..7477bc1 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/RequestActEmbConfig.java @@ -0,0 +1,21 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.RequestActEmbTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +// STEP 4. +@Configuration +public class RequestActEmbConfig { + @Bean + public Step step4_ActEmb(JobRepository repo, PlatformTransactionManager tx, + RequestActEmbTasklet tasklet) { + return new StepBuilder("step4_ActEmb", repo) + .tasklet(tasklet, tx) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/RequestUserEmbConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/RequestUserEmbConfig.java new file mode 100644 index 0000000..8bcecf3 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/RequestUserEmbConfig.java @@ -0,0 +1,21 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.RequestUserEmbTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +// STEP 3 +@Configuration +public class RequestUserEmbConfig { + @Bean + public Step step3_UserEmb(JobRepository repo, PlatformTransactionManager tx, + RequestUserEmbTasklet tasklet) { + return new StepBuilder("step3_UserEmb", repo) + .tasklet(tasklet, tx) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/SaveTopRankConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/SaveTopRankConfig.java new file mode 100644 index 0000000..7b3c52f --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/SaveTopRankConfig.java @@ -0,0 +1,20 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.SaveTopRankTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class SaveTopRankConfig { + @Bean + public Step step2_Rank(JobRepository repo, PlatformTransactionManager tx, + SaveTopRankTasklet tasklet) { + return new StepBuilder("step2_Rank", repo) + .tasklet(tasklet, tx) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/step/UpdateGraphConfig.java b/src/main/java/com/example/silverbridgeX_user/batch/step/UpdateGraphConfig.java new file mode 100644 index 0000000..00213c5 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/step/UpdateGraphConfig.java @@ -0,0 +1,20 @@ +package com.example.silverbridgeX_user.batch.step; + +import com.example.silverbridgeX_user.batch.tasklet.UpdateGraphTasklet; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class UpdateGraphConfig { + @Bean + public Step step5_Graph(JobRepository repo, PlatformTransactionManager tx, + UpdateGraphTasklet tasklet) { + return new StepBuilder("step5_Graph", repo) + .tasklet(tasklet, tx) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java new file mode 100644 index 0000000..6dfb8aa --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java @@ -0,0 +1,40 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +// STEP 1. CTR 계산, 저장 +@Component +@RequiredArgsConstructor +public class CalculateCtrTasklet implements Tasklet { + + private final JdbcTemplate jdbc; + + @Override + public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + String sql = """ + UPDATE activity a SET + click_num = sub.click, + impression_num = sub.impression, + ctr = CASE WHEN sub.impression > 0 + THEN sub.click::float / sub.impression + ELSE 0 END + FROM ( + SELECT activity_id, + COUNT(*) FILTER (WHERE action_type = 1) AS click, + COUNT(*) FILTER (WHERE action_type = 0) AS impression + FROM activity_log + GROUP BY activity_id + ) sub + WHERE a.id = sub.activity_id + """; + + jdbc.update(sql); + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java new file mode 100644 index 0000000..9901118 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java @@ -0,0 +1,22 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +// STEP 7. 최종 추천 리스트 생성 및 저장 +@Component +@RequiredArgsConstructor +public class GenRecommendationTasklet implements Tasklet { + private final RecommendActivityService recService; + + @Override + public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + recService.generateAllUsersRecommendation(); + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java new file mode 100644 index 0000000..6ab050b --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java @@ -0,0 +1,27 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +// STEP 6. 불필요한 간선 제거 +@Component +@RequiredArgsConstructor +public class PruneEdgeTasklet implements Tasklet { + private final RecommendActivityService recommendActivityService; + + @Override + public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + String del = """ + MATCH (:User)-[r:PREFERRED]->(:Activity) + WHERE r.score < 0.4 + DELETE r + """; + recommendActivityService.execWrite(del); + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java new file mode 100644 index 0000000..91cfeb4 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java @@ -0,0 +1,27 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import com.example.silverbridgeX_user.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +// STEP 4. 사용자별 SELECT로 연결된 활동 임베딩들의 평균값 계산 -> actEmb 계산 +@Slf4j +@Component +@RequiredArgsConstructor +public class RequestActEmbTasklet implements Tasklet { + + private final UserRepository userRepository; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + + int updatedCount = userRepository.updateActivityEmbeddingAvgForUsers(); + log.info("사용자 활동 임베딩 평균 업데이트 완료: " + updatedCount + " rows affected"); + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java new file mode 100644 index 0000000..f6cb191 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java @@ -0,0 +1,55 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import com.example.silverbridgeX_user.global.util.EmbeddingClient; +import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.repository.UserRepository; +import com.example.silverbridgeX_user.user.service.UserService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +// STEP 3. 사용자별 갱신된 keywords로 prefEmb 구해서 갱신 +@Component +@RequiredArgsConstructor +public class RequestUserEmbTasklet implements Tasklet { + + private final UserRepository userRepo; + private final EmbeddingClient embeddingClient; + private final UserService userService; + + @Override + public RepeatStatus execute(StepContribution con, ChunkContext ctx) throws Exception { + + // 사용자별 선호 키워드 업데이트 + userService.updatePreferredKeywords(); + + List users = userRepo.findAll(); + for (User user : users) { + + List keywords = user.getPreferredKeywords(); + if (keywords == null || keywords.isEmpty()) { + continue; + } + + // 키워드들 각각을 임베딩하고, max pooling 값 저장 + String vectorLiteral = embeddingClient.getMaxPooledEmbeddingLiteral(keywords); + + try { + // PostgreSQL vector 타입으로 인식시키기 위해 문자열 그대로 저장 + userRepo.updatePreferredEmbedding(user.getId(), vectorLiteral); + System.out.println("success"); + } catch (Exception e) { + System.err.println("유저 임베딩 실패: " + user.getId()); + e.printStackTrace(); + } + } + + return RepeatStatus.FINISHED; + } + +} + diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java new file mode 100644 index 0000000..c3631d9 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java @@ -0,0 +1,36 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +// STEP 2. CTR 높은 순으로 랭킹 매기기 +@Component +@RequiredArgsConstructor +public class SaveTopRankTasklet implements Tasklet { + + private final JdbcTemplate jdbc; + + @Override + public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + jdbc.update("DELETE FROM activity_ranking"); + jdbc.update(""" + INSERT INTO activity_ranking (activity_id, rank) + SELECT id, rank + FROM ( + SELECT id, + RANK() OVER (ORDER BY ctr DESC) AS rank + FROM activity + WHERE ctr > 0 + ) sub + ORDER BY rank + LIMIT 50 + """); + return RepeatStatus.FINISHED; + } +} + diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java new file mode 100644 index 0000000..88e2dc8 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java @@ -0,0 +1,97 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +// STEP 5. 임베딩 기반 그래프 갱신 +@Component +@RequiredArgsConstructor +public class UpdateGraphTasklet implements Tasklet { + + private final NamedParameterJdbcTemplate jdbc; // PostgreSQL 접근용 + private final RecommendActivityService recommendSvc; // Neo4j 간선 INSERT 담당 + + @Override + public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + + /*************** 1) 사용자 → 활동 :PREFERRED (Top-2) ****************/ + String preferredSql = """ + WITH cand AS ( + SELECT + u.id AS user_id, + a.id AS activity_id, + 1 - ( 0.3 * cosine_distance(u.preferred_embedding, a.description_embedding) + + 0.7 * cosine_distance(u.activity_embedding_avg, a.description_embedding) ) AS score, + row_number() OVER (PARTITION BY u.id + ORDER BY 1 - ( 0.3 * cosine_distance(u.preferred_embedding, a.description_embedding) + + 0.7 * cosine_distance(u.activity_embedding_avg, a.description_embedding) ) DESC) AS rk + FROM member u + CROSS JOIN activity a + WHERE u.preferred_embedding IS NOT NULL + AND u.activity_embedding_avg IS NOT NULL + AND a.description_embedding IS NOT NULL + -- 거리 < 10 km + AND earth_distance( + ll_to_earth(u.latitude ::float, u.longitude ::float), + ll_to_earth(a.latitude ::float, a.longitude ::float) + ) < 10000 + -- 이미 SELECTED 인 활동 제외 + AND NOT EXISTS ( + SELECT 1 FROM activity_log al + WHERE al.user_id = u.id + AND al.activity_id = a.id + AND al.action_type = 1 + ) + ) + SELECT user_id, activity_id, score + FROM cand + WHERE score >= 0.5 + AND rk <= 2; + """; + + List> prefRows = jdbc.queryForList(preferredSql, Map.of()); + recommendSvc.insertPreferredEdges(prefRows); // Neo4j MERGE (u)-[:PREFERRED]->(a) + + /*************** 2) 사용자 ↔ 사용자 :SIMILAR ****************/ + String similarSql = """ + WITH pair AS ( + SELECT + u1.id AS uid1, + u2.id AS uid2, + 1 - cosine_distance(u1.preferred_embedding, u2.preferred_embedding ) AS pref_sim, + 1 - cosine_distance(u1.activity_embedding_avg, u2.activity_embedding_avg ) AS act_sim, + (SELECT COUNT(*) FROM activity_log al WHERE al.user_id = u1.id AND al.action_type = 1) AS cnt1, + (SELECT COUNT(*) FROM activity_log al WHERE al.user_id = u2.id AND al.action_type = 1) AS cnt2 + FROM member u1 + JOIN member u2 ON u1.id < u2.id + WHERE u1.preferred_embedding IS NOT NULL + AND u2.preferred_embedding IS NOT NULL + ) + SELECT + uid1, uid2, + CASE + WHEN cnt1 = 0 AND cnt2 > 0 THEN pref_sim -- cold-start + ELSE 0.7 * pref_sim + 0.3 * act_sim + END AS w_sim + FROM pair + WHERE + (CASE + WHEN cnt1 = 0 AND cnt2 > 0 THEN pref_sim + ELSE 0.7 * pref_sim + 0.3 * act_sim + END) >= 0.5; + """; + + List> simRows = jdbc.queryForList(similarSql, Map.of()); + recommendSvc.insertSimilarEdges(simRows); // Neo4j MERGE (u1)-[:SIMILAR]-(u2) + + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/silverbridgeX_user/global/config/RestTemplateConfig.java b/src/main/java/com/example/silverbridgeX_user/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..6bf3e19 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/global/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package com.example.silverbridgeX_user.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/global/config/SchedulerConfig.java b/src/main/java/com/example/silverbridgeX_user/global/config/SchedulerConfig.java new file mode 100644 index 0000000..84d3e96 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/global/config/SchedulerConfig.java @@ -0,0 +1,15 @@ +package com.example.silverbridgeX_user.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +public class SchedulerConfig { + + @Bean + public TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java b/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java index d03192e..bb49a77 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java +++ b/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java @@ -5,6 +5,7 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.List; import java.util.Scanner; import java.util.stream.Collectors; import java.util.stream.DoubleStream; @@ -14,7 +15,7 @@ public class EmbeddingClient { public String getEmbeddingLiteral(String text) throws Exception { - URL url = new URL("http://localhost:8001/embed"); + URL url = new URL("http://localhost:8000/embedding"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); @@ -37,4 +38,40 @@ public String getEmbeddingLiteral(String text) throws Exception { .mapToObj(Double::toString) .collect(Collectors.joining(", ")) + "]"; } + + public String getMaxPooledEmbeddingLiteral(List keywords) throws Exception { + if (keywords == null || keywords.isEmpty()) { + return null; + } + + // 각 키워드 임베딩 추출 + double[][] embeddings = new double[keywords.size()][]; + + for (int i = 0; i < keywords.size(); i++) { + String keyword = keywords.get(i); + String vectorStr = getEmbeddingLiteral(keyword); // "[0.1, 0.2, ...]" + String[] parts = vectorStr.replace("[", "").replace("]", "").split(","); + embeddings[i] = new double[parts.length]; + for (int j = 0; j < parts.length; j++) { + embeddings[i][j] = Double.parseDouble(parts[j].trim()); + } + } + + // max pooling 계산 + int dim = embeddings[0].length; + double[] maxPooled = new double[dim]; + for (int d = 0; d < dim; d++) { + double max = Double.NEGATIVE_INFINITY; + for (double[] vec : embeddings) { + max = Math.max(max, vec[d]); + } + maxPooled[d] = max; + } + + // PostgreSQL vector 타입용 문자열 생성 + return "[" + DoubleStream.of(maxPooled) + .mapToObj(d -> String.format("%.6f", d)) + .collect(Collectors.joining(", ")) + "]"; + } + } \ No newline at end of file diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index f6725e2..fec31a4 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -60,6 +60,9 @@ public class User extends BaseEntity { @JdbcTypeCode(SqlTypes.JSON) private List dislikeKeywords; + @Column(name = "activity_embedding_avg", columnDefinition = "vector(384)", insertable = false, updatable = false) + private String activityEmbeddingAvg; + @Column(name = "preferred_embedding", columnDefinition = "vector(384)", insertable = false, updatable = false) private String preferredEmbedding; @@ -76,6 +79,9 @@ public void updateNickname(String nickname) { this.nickname = nickname; } + public void updatePreferredKeywords(List keywords) { + this.preferredKeywords = keywords; + } } diff --git a/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java b/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java index 0f7e5d7..298078c 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java @@ -3,18 +3,48 @@ import com.example.silverbridgeX_user.user.domain.User; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository public interface UserRepository extends JpaRepository { // 1. 사용자 계정이름으로 사용자 정보를 회수하는 기능 Optional findByUsername(String username); + // 2. 사용자 이메일으로 사용자 정보를 회수하는 기능 Optional findByEmail(String email); + // 3. 사용자 계정이름을 가진 사용자 정보가 존재하는지 판단하는 기능 boolean existsByUsername(String username); + // 4. 사용자 이메일을 가진 사용자 정보가 존재하는지 판단하는 기능 boolean existsByEmail(String email); + // 5. 닉네임이 사용중인지 판단하는 기능 boolean existsByNickname(String nickname); + + @Modifying + @Transactional + @Query(value = "UPDATE member SET preferred_embedding = CAST(:embedding AS vector) WHERE id = :userId", nativeQuery = true) + void updatePreferredEmbedding(@Param("userId") Long userId, @Param("embedding") String embedding); + + @Modifying(clearAutomatically = true) + @Transactional + @Query(value = """ + UPDATE member u + SET activity_embedding_avg = CAST(sub.avg_vec AS vector) + FROM ( + SELECT al.user_id, + avg(a.description_embedding) AS avg_vec + FROM activity_log al + JOIN activity a ON al.activity_id = a.id + WHERE al.action_type = 1 AND a.description_embedding IS NOT NULL + GROUP BY al.user_id + ) sub + WHERE u.id = sub.user_id + """, nativeQuery = true) + int updateActivityEmbeddingAvgForUsers(); } \ No newline at end of file From d20cba785c9fa2625bdd7149507dcde126ff65e4 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:23:23 +0900 Subject: [PATCH 30/41] =?UTF-8?q?feat:=20enum=20=EC=BB=AC=EB=9F=BC=20not?= =?UTF-8?q?=20null=20=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=AC=B4=EA=B2=B0=EC=84=B1=20?= =?UTF-8?q?=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/Activity.java | 1 + .../activity/domain/ActivityLog.java | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index 9fb0f13..003bad0 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -24,6 +24,7 @@ public class Activity { @Column(unique = true) private String name; + @Column(nullable = false) private ActivityType activityType; @Column(length = 1000) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java index a223e3e..d418e1b 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java @@ -1,9 +1,20 @@ package com.example.silverbridgeX_user.activity.domain; import com.example.silverbridgeX_user.user.domain.User; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import java.time.LocalDateTime; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @@ -20,6 +31,7 @@ public class ActivityLog { @JoinColumn(name = "user_id") private User user; + @Column(nullable = false) private ActionType actionType; private LocalDateTime time; From 0c5948341981e2e39c520eacab6f86ede5d3968a Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:24:39 +0900 Subject: [PATCH 31/41] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=8B=A4=ED=96=89=20=EC=8B=9C?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20Spring=20Batch=EC=9A=A9=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EB=93=A4=EC=9D=84=20DROP=20=ED=9B=84=20CREAT?= =?UTF-8?q?E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0dbaf1b..f0f31d5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,6 +27,11 @@ neo4j: username: ${neo4j.authentication.username} password: ${neo4j.authentication.password} +logging: + level: + org.springframework.batch: DEBUG + org.springframework.jdbc.datasource.init: DEBUG + --- # local profile @@ -39,6 +44,9 @@ spring: username: ${local.db.username} password: ${local.db.password} driver-class-name: org.postgresql.Driver + batch: + jdbc: + initialize-schema: always sql: init: mode: never From 450d02b2fa3e82f2b03e655999743dbb46c70147 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:26:01 +0900 Subject: [PATCH 32/41] =?UTF-8?q?feat:=20@CreationTimestamp=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=EA=B0=80=20=EC=B2=98=EC=9D=8C=20=EC=A0=80=EC=9E=A5(persist)?= =?UTF-8?q?=EB=90=A0=20=EB=95=8C=EC=9D=98=20=EC=83=9D=EC=84=B1=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/activity/domain/ActivityRanking.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java index e26283d..b0140f5 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityRanking.java @@ -12,6 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; @Entity @Getter @@ -29,5 +30,6 @@ public class ActivityRanking { private Integer rank; + @CreationTimestamp private LocalDate createdAt; } From f9d3eb3431a14d319efb22e1b232dda36ce2d6dc Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:30:52 +0900 Subject: [PATCH 33/41] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20name?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/silverbridgeX_user/global/util/EmbeddingClient.java | 2 +- .../example/silverbridgeX_user/user/service/UserService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java b/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java index bb49a77..ec974af 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java +++ b/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java @@ -15,7 +15,7 @@ public class EmbeddingClient { public String getEmbeddingLiteral(String text) throws Exception { - URL url = new URL("http://localhost:8000/embedding"); + URL url = new URL("http://ai-server:8000/embedding"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index 0f081fc..a5e69d3 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -36,7 +36,7 @@ public class UserService { private final RestTemplate restTemplate; private final Driver driver; - private final String PREF_URL = "http://localhost:3000/summary/preferences/all"; + private final String PREF_URL = "http://chat-server:3000/summary/preferences/all"; @Transactional public User findByUserName(String userName) { From 0eef9ca92e8a0cefde3bc18b2751ae99c0486ad9 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:38:30 +0900 Subject: [PATCH 34/41] =?UTF-8?q?refactor:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EC=97=90=20@NonNull=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java | 3 ++- .../batch/tasklet/GenRecommendationTasklet.java | 3 ++- .../silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java | 3 ++- .../silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java | 3 ++- .../batch/tasklet/RequestUserEmbTasklet.java | 3 ++- .../silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java | 3 ++- .../silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java index 6dfb8aa..27a0a79 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java @@ -1,5 +1,6 @@ package com.example.silverbridgeX_user.batch.tasklet; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -16,7 +17,7 @@ public class CalculateCtrTasklet implements Tasklet { private final JdbcTemplate jdbc; @Override - public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ctx) { String sql = """ UPDATE activity a SET click_num = sub.click, diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java index 9901118..1748885 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTasklet.java @@ -1,6 +1,7 @@ package com.example.silverbridgeX_user.batch.tasklet; import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -15,7 +16,7 @@ public class GenRecommendationTasklet implements Tasklet { private final RecommendActivityService recService; @Override - public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ctx) { recService.generateAllUsersRecommendation(); return RepeatStatus.FINISHED; } diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java index 6ab050b..fa79bdf 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTasklet.java @@ -1,6 +1,7 @@ package com.example.silverbridgeX_user.batch.tasklet; import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -15,7 +16,7 @@ public class PruneEdgeTasklet implements Tasklet { private final RecommendActivityService recommendActivityService; @Override - public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ctx) { String del = """ MATCH (:User)-[r:PREFERRED]->(:Activity) WHERE r.score < 0.4 diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java index 91cfeb4..f2a9572 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTasklet.java @@ -1,6 +1,7 @@ package com.example.silverbridgeX_user.batch.tasklet; import com.example.silverbridgeX_user.user.repository.UserRepository; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; @@ -18,7 +19,7 @@ public class RequestActEmbTasklet implements Tasklet { private final UserRepository userRepository; @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + public RepeatStatus execute(@NonNull StepContribution contribution, @NonNull ChunkContext chunkContext) { int updatedCount = userRepository.updateActivityEmbeddingAvgForUsers(); log.info("사용자 활동 임베딩 평균 업데이트 완료: " + updatedCount + " rows affected"); diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java index f6cb191..e978fad 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTasklet.java @@ -5,6 +5,7 @@ import com.example.silverbridgeX_user.user.repository.UserRepository; import com.example.silverbridgeX_user.user.service.UserService; import java.util.List; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -22,7 +23,7 @@ public class RequestUserEmbTasklet implements Tasklet { private final UserService userService; @Override - public RepeatStatus execute(StepContribution con, ChunkContext ctx) throws Exception { + public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ctx) throws Exception { // 사용자별 선호 키워드 업데이트 userService.updatePreferredKeywords(); diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java index c3631d9..b21566d 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTasklet.java @@ -1,5 +1,6 @@ package com.example.silverbridgeX_user.batch.tasklet; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -16,7 +17,7 @@ public class SaveTopRankTasklet implements Tasklet { private final JdbcTemplate jdbc; @Override - public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ctx) { jdbc.update("DELETE FROM activity_ranking"); jdbc.update(""" INSERT INTO activity_ranking (activity_id, rank) diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java index 88e2dc8..7fcf7db 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java @@ -3,6 +3,7 @@ import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; import java.util.List; import java.util.Map; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -20,7 +21,7 @@ public class UpdateGraphTasklet implements Tasklet { private final RecommendActivityService recommendSvc; // Neo4j 간선 INSERT 담당 @Override - public RepeatStatus execute(StepContribution con, ChunkContext ctx) { + public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ctx) { /*************** 1) 사용자 → 활동 :PREFERRED (Top-2) ****************/ String preferredSql = """ From ff61451dd6715caf61a5620a0cdb53c91d0f6052 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 12 Jun 2025 23:58:23 +0900 Subject: [PATCH 35/41] =?UTF-8?q?refactor:=20URL=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/global/util/EmbeddingClient.java | 5 ++++- .../example/silverbridgeX_user/user/service/UserService.java | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java b/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java index ec974af..85658b6 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java +++ b/src/main/java/com/example/silverbridgeX_user/global/util/EmbeddingClient.java @@ -9,13 +9,16 @@ import java.util.Scanner; import java.util.stream.Collectors; import java.util.stream.DoubleStream; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class EmbeddingClient { + @Value("${ai.server.url}") + private String aiServerUrl; public String getEmbeddingLiteral(String text) throws Exception { - URL url = new URL("http://ai-server:8000/embedding"); + URL url = new URL(aiServerUrl + "/embedding"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index a5e69d3..3aaace2 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; @@ -36,7 +37,9 @@ public class UserService { private final RestTemplate restTemplate; private final Driver driver; - private final String PREF_URL = "http://chat-server:3000/summary/preferences/all"; + @Value("${chat.server.url}") + private String chatServerUrl; + private final String PREF_URL = chatServerUrl + "/summary/preferences/all"; @Transactional public User findByUserName(String userName) { From 7e66a089cc59208cd972224e0d9b5d8ca70c6439 Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 13 Jun 2025 00:12:40 +0900 Subject: [PATCH 36/41] =?UTF-8?q?bug=20fix:=20final=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=20PREF=5FURL=EC=9D=84=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=ED=95=98=EB=A9=B4=EC=84=9C=20cha?= =?UTF-8?q?tServerUrl=EC=9D=B4=20=EC=95=84=EC=A7=81=20null=EC=9D=B8=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/user/service/UserService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index 3aaace2..ada5a3c 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -11,6 +11,7 @@ import com.example.silverbridgeX_user.user.jwt.JwtTokenUtils; import com.example.silverbridgeX_user.user.repository.RefreshTokenRepository; import com.example.silverbridgeX_user.user.repository.UserRepository; +import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import java.util.List; @@ -39,7 +40,12 @@ public class UserService { @Value("${chat.server.url}") private String chatServerUrl; - private final String PREF_URL = chatServerUrl + "/summary/preferences/all"; + private String PREF_URL; + + @PostConstruct + public void init() { + this.PREF_URL = chatServerUrl + "/summary/preferences/all"; + } @Transactional public User findByUserName(String userName) { From 3138f5d09fbb897264da08d694e52e81952a58c1 Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 13 Jun 2025 11:12:50 +0900 Subject: [PATCH 37/41] =?UTF-8?q?feat:=20=EC=A2=8C=ED=91=9C=EA=B0=92=20?= =?UTF-8?q?=EC=96=BB=EB=8A=94=20api=20key=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/service/ActivityInserter.java | 22 +++++++++++++++++++ .../activity/service/ActivityService.java | 8 ++++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/activity/service/ActivityInserter.java diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityInserter.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityInserter.java new file mode 100644 index 0000000..ad8f05a --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityInserter.java @@ -0,0 +1,22 @@ +package com.example.silverbridgeX_user.activity.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ActivityInserter implements ApplicationRunner { + private final ActivityService activityService; + + @Override + public void run(ApplicationArguments args) throws Exception { +// System.out.println("🌟 ApplicationRunner로 실행 중"); +// activityService.fetchAndSaveFestivalData(); +// activityService.fetchAndSaveTourSpotData(); +// activityService.fetchAndSaveCoordinate(); +// activityService.insertActivitiesNeo4j(); +// System.out.println("insert finish"); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java index 590892a..aa407e7 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -27,6 +27,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Values; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Slf4j @@ -196,7 +197,7 @@ public void fetchAndSaveCoordinate() throws Exception { } String json = activityApiService.getJsonFromUrl(urlStr); - System.out.println(json); + log.info(json); JsonNode point = activityApiService.parsePoint(json); if (!point.isMissingNode()) { @@ -214,13 +215,14 @@ public void fetchAndSaveCoordinate() throws Exception { } - String key = ""; + @Value("${geocoder.api.key}") + private String key; private String buildCoordinateUrl(String type, String address) throws Exception { StringBuilder urlBuilder = new StringBuilder(addressToCoordinateApiurl); urlBuilder.append("&key=" + key); urlBuilder.append("&type=" + type); // PARCEL : 지번주소, ROAD : 도로명주소 - urlBuilder.append("&address=" + URLEncoder.encode("경상북도 영주시 봉현면 오현리 763-1", StandardCharsets.UTF_8)); + urlBuilder.append("&address=" + URLEncoder.encode(address, StandardCharsets.UTF_8)); return urlBuilder.toString(); } From 199ab57421d4182d9e3c9efbb73d1911a7138cde Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 13 Jun 2025 11:50:01 +0900 Subject: [PATCH 38/41] =?UTF-8?q?feat:=20@Enumerated(EnumType.STRING)=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecommendActivity/domain/ActionType.java | 6 ++++++ .../{activity => RecommendActivity}/domain/ActivityLog.java | 6 +++++- .../silverbridgeX_user/activity/domain/ActionType.java | 6 ------ .../silverbridgeX_user/activity/domain/Activity.java | 3 +++ 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActionType.java rename src/main/java/com/example/silverbridgeX_user/{activity => RecommendActivity}/domain/ActivityLog.java (81%) delete mode 100644 src/main/java/com/example/silverbridgeX_user/activity/domain/ActionType.java diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActionType.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActionType.java new file mode 100644 index 0000000..2d46196 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActionType.java @@ -0,0 +1,6 @@ +package com.example.silverbridgeX_user.RecommendActivity.domain; + +public enum ActionType { + VIEW, + SELECT +} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActivityLog.java similarity index 81% rename from src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java rename to src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActivityLog.java index d418e1b..545bea7 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActivityLog.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/domain/ActivityLog.java @@ -1,8 +1,11 @@ -package com.example.silverbridgeX_user.activity.domain; +package com.example.silverbridgeX_user.RecommendActivity.domain; +import com.example.silverbridgeX_user.activity.domain.Activity; import com.example.silverbridgeX_user.user.domain.User; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -31,6 +34,7 @@ public class ActivityLog { @JoinColumn(name = "user_id") private User user; + @Enumerated(EnumType.STRING) @Column(nullable = false) private ActionType actionType; diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActionType.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/ActionType.java deleted file mode 100644 index d889c8d..0000000 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/ActionType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.silverbridgeX_user.activity.domain; - -public enum ActionType { - VIEW, - SELECT -} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java index 003bad0..3a83503 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/domain/Activity.java @@ -2,6 +2,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -24,6 +26,7 @@ public class Activity { @Column(unique = true) private String name; + @Enumerated(EnumType.STRING) @Column(nullable = false) private ActivityType activityType; From 8eb921a85b5ee5901aebce90f47564e9c6ffa9cd Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 13 Jun 2025 12:23:53 +0900 Subject: [PATCH 39/41] =?UTF-8?q?refactor:=20String=EC=9D=B8=20ActionType?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=BF=BC=EB=A6=AC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecommendActivity/service/RecommendActivityService.java | 4 ++-- .../activity/converter/ActivityLogConverter.java | 6 +++--- .../activity/repository/ActivityLogRepository.java | 2 +- .../batch/tasklet/CalculateCtrTasklet.java | 4 ++-- .../batch/tasklet/UpdateGraphTasklet.java | 6 +++--- .../silverbridgeX_user/user/repository/UserRepository.java | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java index 5b6592e..ef47c35 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java @@ -1,11 +1,11 @@ package com.example.silverbridgeX_user.RecommendActivity.service; import com.example.silverbridgeX_user.RecommendActivity.converter.RecommendActivityConverter; +import com.example.silverbridgeX_user.RecommendActivity.domain.ActivityLog; import com.example.silverbridgeX_user.RecommendActivity.domain.RecommendActivity; import com.example.silverbridgeX_user.RecommendActivity.repository.RecommendActivityRepository; import com.example.silverbridgeX_user.activity.converter.ActivityLogConverter; import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.activity.domain.ActivityLog; import com.example.silverbridgeX_user.activity.repository.ActivityLogRepository; import com.example.silverbridgeX_user.activity.repository.ActivityRankingRepository; import com.example.silverbridgeX_user.activity.repository.ActivityRepository; @@ -155,7 +155,7 @@ public void insertSimilarEdges(List> rows) { public void generateAllUsersRecommendation() { // 0) 모든 사용자 조회 (PostgreSQL) - List users = activityRepository.findAllUsers(); // → 필요 메서드 구현 + List users = activityRepository.findAllUsers(); try (Session session = neo4jDriver.session()) { diff --git a/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java b/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java index ab793ff..df54aff 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/converter/ActivityLogConverter.java @@ -1,8 +1,8 @@ package com.example.silverbridgeX_user.activity.converter; -import com.example.silverbridgeX_user.activity.domain.ActionType; +import com.example.silverbridgeX_user.RecommendActivity.domain.ActionType; +import com.example.silverbridgeX_user.RecommendActivity.domain.ActivityLog; import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.activity.domain.ActivityLog; import com.example.silverbridgeX_user.user.domain.User; import java.time.LocalDateTime; import java.util.Objects; @@ -10,7 +10,7 @@ public class ActivityLogConverter { public static ActivityLog saveActivityLog(User user, Activity activity, LocalDateTime now, String type) { ActionType actionType = null; - if (Objects.equals(type, "View")) { + if (Objects.equals(type, "VIEW")) { actionType = ActionType.VIEW; } else if (Objects.equals(type, "SELECT")) { actionType = ActionType.SELECT; diff --git a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityLogRepository.java b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityLogRepository.java index de014b4..4185810 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityLogRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityLogRepository.java @@ -1,6 +1,6 @@ package com.example.silverbridgeX_user.activity.repository; -import com.example.silverbridgeX_user.activity.domain.ActivityLog; +import com.example.silverbridgeX_user.RecommendActivity.domain.ActivityLog; import org.springframework.data.jpa.repository.JpaRepository; public interface ActivityLogRepository extends JpaRepository { diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java index 27a0a79..07ea4cf 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTasklet.java @@ -27,8 +27,8 @@ public RepeatStatus execute(@NonNull StepContribution con, @NonNull ChunkContext ELSE 0 END FROM ( SELECT activity_id, - COUNT(*) FILTER (WHERE action_type = 1) AS click, - COUNT(*) FILTER (WHERE action_type = 0) AS impression + COUNT(*) FILTER (WHERE action_type = 'SELECT') AS click, + COUNT(*) FILTER (WHERE action_type = 'VIEW') AS impression FROM activity_log GROUP BY activity_id ) sub diff --git a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java index 7fcf7db..310ea34 100644 --- a/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java +++ b/src/main/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTasklet.java @@ -49,7 +49,7 @@ AND NOT EXISTS ( SELECT 1 FROM activity_log al WHERE al.user_id = u.id AND al.activity_id = a.id - AND al.action_type = 1 + AND al.action_type = 'SELECT' ) ) SELECT user_id, activity_id, score @@ -69,8 +69,8 @@ WITH pair AS ( u2.id AS uid2, 1 - cosine_distance(u1.preferred_embedding, u2.preferred_embedding ) AS pref_sim, 1 - cosine_distance(u1.activity_embedding_avg, u2.activity_embedding_avg ) AS act_sim, - (SELECT COUNT(*) FROM activity_log al WHERE al.user_id = u1.id AND al.action_type = 1) AS cnt1, - (SELECT COUNT(*) FROM activity_log al WHERE al.user_id = u2.id AND al.action_type = 1) AS cnt2 + (SELECT COUNT(*) FROM activity_log al WHERE al.user_id = u1.id AND al.action_type = 'SELECT') AS cnt1, + (SELECT COUNT(*) FROM activity_log al WHERE al.user_id = u2.id AND al.action_type = 'SELECT') AS cnt2 FROM member u1 JOIN member u2 ON u1.id < u2.id WHERE u1.preferred_embedding IS NOT NULL diff --git a/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java b/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java index 298078c..aec249c 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java @@ -41,7 +41,7 @@ public interface UserRepository extends JpaRepository { avg(a.description_embedding) AS avg_vec FROM activity_log al JOIN activity a ON al.activity_id = a.id - WHERE al.action_type = 1 AND a.description_embedding IS NOT NULL + WHERE al.action_type = 'SELECT' AND a.description_embedding IS NOT NULL GROUP BY al.user_id ) sub WHERE u.id = sub.user_id From 9f323d58a65d65a5b5b55a051832a6458d6918b5 Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 13 Jun 2025 14:29:51 +0900 Subject: [PATCH 40/41] =?UTF-8?q?test:=20spring=20batch=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tasklet/CalculateCtrTaskletTest.java | 29 +++++ .../tasklet/GenRecommendationTaskletTest.java | 25 ++++ .../batch/tasklet/PruneEdgeTaskletTest.java | 25 ++++ .../tasklet/RequestActEmbTaskletTest.java | 29 +++++ .../tasklet/RequestUserEmbTaskletTest.java | 109 ++++++++++++++++++ .../batch/tasklet/SaveTopRankTaskletTest.java | 30 +++++ .../batch/tasklet/UpdateGraphTaskletTest.java | 32 +++++ 7 files changed, 279 insertions(+) create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTaskletTest.java create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTaskletTest.java create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTaskletTest.java create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTaskletTest.java create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTaskletTest.java create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTaskletTest.java diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTaskletTest.java new file mode 100644 index 0000000..f4d1718 --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/CalculateCtrTaskletTest.java @@ -0,0 +1,29 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.jdbc.core.JdbcTemplate; + +public class CalculateCtrTaskletTest { + @Test + void execute_updatesCtrSuccessfully() throws Exception { + // Given + JdbcTemplate mockJdbc = mock(JdbcTemplate.class); + CalculateCtrTasklet tasklet = new CalculateCtrTasklet(mockJdbc); + StepContribution mockCon = mock(StepContribution.class); + ChunkContext mockCtx = mock(ChunkContext.class); + + // When + tasklet.execute(mockCon, mockCtx); + + // Then + verify(mockJdbc, times(1)).update(anyString()); + } + +} diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTaskletTest.java new file mode 100644 index 0000000..08367fb --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/GenRecommendationTaskletTest.java @@ -0,0 +1,25 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class GenRecommendationTaskletTest { + @Test + void execute_generatesRecommendation() throws Exception { + // Given + RecommendActivityService mockService = mock(RecommendActivityService.class); + GenRecommendationTasklet tasklet = new GenRecommendationTasklet(mockService); + + // When + tasklet.execute(mock(StepContribution.class), mock(ChunkContext.class)); + + // Then + verify(mockService, times(1)).generateAllUsersRecommendation(); + } +} diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTaskletTest.java new file mode 100644 index 0000000..3508b90 --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/PruneEdgeTaskletTest.java @@ -0,0 +1,25 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class PruneEdgeTaskletTest { + @Test + void execute_deletesLowScoreEdges() throws Exception { + // Given + RecommendActivityService mockService = mock(RecommendActivityService.class); + PruneEdgeTasklet tasklet = new PruneEdgeTasklet(mockService); + + // When + tasklet.execute(mock(StepContribution.class), mock(ChunkContext.class)); + + // Then + verify(mockService).execWrite(anyString()); + } +} diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTaskletTest.java new file mode 100644 index 0000000..290b6ca --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestActEmbTaskletTest.java @@ -0,0 +1,29 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.silverbridgeX_user.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class RequestActEmbTaskletTest { + @Test + void execute_updatesEmbeddingAvg() throws Exception { + // Given + UserRepository mockRepo = mock(UserRepository.class); + when(mockRepo.updateActivityEmbeddingAvgForUsers()).thenReturn(3); + + RequestActEmbTasklet tasklet = new RequestActEmbTasklet(mockRepo); + + // When + tasklet.execute(mock(StepContribution.class), mock(ChunkContext.class)); + + // Then + verify(mockRepo, times(1)).updateActivityEmbeddingAvgForUsers(); + } + +} diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java new file mode 100644 index 0000000..c93dd4a --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java @@ -0,0 +1,109 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyList; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.silverbridgeX_user.global.util.EmbeddingClient; +import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.repository.UserRepository; +import com.example.silverbridgeX_user.user.service.UserService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.repeat.RepeatStatus; + +public class RequestUserEmbTaskletTest { + + @InjectMocks + private RequestUserEmbTasklet tasklet; + + @Mock + private UserRepository userRepository; + + @Mock + private EmbeddingClient embeddingClient; + + @Mock + private UserService userService; + + @Mock + private StepContribution contribution; + + @Mock + private ChunkContext chunkContext; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void execute_successfully_updates_embeddings() throws Exception { + // Given + User user = mock(User.class); + when(user.getId()).thenReturn(1L); + when(user.getPreferredKeywords()).thenReturn(List.of("문화", "자연")); + + when(userRepository.findAll()).thenReturn(List.of(user)); + when(embeddingClient.getMaxPooledEmbeddingLiteral(anyList())) + .thenReturn("'[0.1, 0.2, 0.3]'"); + + // When + RepeatStatus status = tasklet.execute(contribution, chunkContext); + + // Then + verify(userService).updatePreferredKeywords(); + verify(userRepository).updatePreferredEmbedding(eq(1L), anyString()); + assertThat(status).isEqualTo(RepeatStatus.FINISHED); + } + + @Test + void execute_skips_user_with_no_keywords() throws Exception { + // Given + User user = mock(User.class); + when(user.getPreferredKeywords()).thenReturn(List.of()); + + when(userRepository.findAll()).thenReturn(List.of(user)); + + // When + RepeatStatus status = tasklet.execute(contribution, chunkContext); + + // Then + verify(userRepository, never()).updatePreferredEmbedding(anyLong(), anyString()); + assertThat(status).isEqualTo(RepeatStatus.FINISHED); + } + + @Test + void execute_handles_exception_during_embedding() throws Exception { + // Given + User user = mock(User.class); + when(user.getId()).thenReturn(99L); + when(user.getPreferredKeywords()).thenReturn(List.of("키워드")); + + when(userRepository.findAll()).thenReturn(List.of(user)); + when(embeddingClient.getMaxPooledEmbeddingLiteral(anyList())) + .thenReturn("'[0.1, 0.2]'"); + doThrow(new RuntimeException("DB Error")) + .when(userRepository).updatePreferredEmbedding(anyLong(), anyString()); + + // When + RepeatStatus status = tasklet.execute(contribution, chunkContext); + + // Then + verify(userRepository).updatePreferredEmbedding(eq(99L), anyString()); + assertThat(status).isEqualTo(RepeatStatus.FINISHED); + } +} diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTaskletTest.java new file mode 100644 index 0000000..45b5ca6 --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/SaveTopRankTaskletTest.java @@ -0,0 +1,30 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.jdbc.core.JdbcTemplate; + +public class SaveTopRankTaskletTest { + @Test + void execute_ranksAndInsertsSuccessfully() throws Exception { + // Given + JdbcTemplate mockJdbc = mock(JdbcTemplate.class); + SaveTopRankTasklet tasklet = new SaveTopRankTasklet(mockJdbc); + + StepContribution mockCon = mock(StepContribution.class); + ChunkContext mockCtx = mock(ChunkContext.class); + + // When + tasklet.execute(mockCon, mockCtx); + + // Then + verify(mockJdbc, times(1)).update("DELETE FROM activity_ranking"); + verify(mockJdbc, times(2)).update(anyString()); + } +} diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTaskletTest.java new file mode 100644 index 0000000..32abf99 --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/UpdateGraphTaskletTest.java @@ -0,0 +1,32 @@ +package com.example.silverbridgeX_user.batch.tasklet; + +import static org.mockito.Mockito.anyList; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.silverbridgeX_user.RecommendActivity.service.RecommendActivityService; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +public class UpdateGraphTaskletTest { + @Test + void execute_updatesGraphEdges() throws Exception { + NamedParameterJdbcTemplate jdbc = mock(NamedParameterJdbcTemplate.class); + RecommendActivityService svc = mock(RecommendActivityService.class); + + when(jdbc.queryForList(anyString(), anyMap())).thenReturn(List.of(Map.of("dummy", "value"))); + + UpdateGraphTasklet tasklet = new UpdateGraphTasklet(jdbc, svc); + tasklet.execute(mock(StepContribution.class), mock(ChunkContext.class)); + + verify(svc).insertPreferredEdges(anyList()); + verify(svc).insertSimilarEdges(anyList()); + } +} From 94bb8d6162edc64aaf2f84ce0b4a03d43cdf7fee Mon Sep 17 00:00:00 2001 From: persi Date: Fri, 13 Jun 2025 14:54:05 +0900 Subject: [PATCH 41/41] =?UTF-8?q?build:=20=ED=85=8C=EC=8A=A4=ED=8C=85?= =?UTF-8?q?=EC=97=90=20kotlin=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 14 ++- .../tasklet/RequestUserEmbTaskletTest.java | 7 +- .../tasklet/RequestUserEmbTaskletTestKt.kt | 89 +++++++++++++++++++ 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTestKt.kt diff --git a/build.gradle b/build.gradle index a92f41a..cb4156a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.9.22' id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version '1.1.6' } @@ -13,6 +14,10 @@ java { } } +kotlin { + jvmToolchain(21) +} + configurations { compileOnly { extendsFrom annotationProcessor @@ -35,6 +40,7 @@ dependencies { // batch implementation 'org.springframework.boot:spring-boot-starter-batch' + testImplementation 'org.springframework.batch:spring-batch-test' // lombok compileOnly 'org.projectlombok:lombok' @@ -49,9 +55,15 @@ dependencies { implementation 'org.postgresql:postgresql:42.7.1' implementation 'org.neo4j.driver:neo4j-java-driver:5.14.0' // implementation 'org.springframework.boot:spring-boot-starter-data-redis' - + // test testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.assertj:assertj-core:3.25.3' + testImplementation 'org.mockito:mockito-core' + testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" + + // kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" // swaggerDoc implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java index c93dd4a..f90d6a3 100644 --- a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,6 +44,9 @@ public class RequestUserEmbTaskletTest { @Mock private ChunkContext chunkContext; + @Mock + private User user; + @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); @@ -53,7 +55,6 @@ void setUp() { @Test void execute_successfully_updates_embeddings() throws Exception { // Given - User user = mock(User.class); when(user.getId()).thenReturn(1L); when(user.getPreferredKeywords()).thenReturn(List.of("문화", "자연")); @@ -73,7 +74,6 @@ void execute_successfully_updates_embeddings() throws Exception { @Test void execute_skips_user_with_no_keywords() throws Exception { // Given - User user = mock(User.class); when(user.getPreferredKeywords()).thenReturn(List.of()); when(userRepository.findAll()).thenReturn(List.of(user)); @@ -89,7 +89,6 @@ void execute_skips_user_with_no_keywords() throws Exception { @Test void execute_handles_exception_during_embedding() throws Exception { // Given - User user = mock(User.class); when(user.getId()).thenReturn(99L); when(user.getPreferredKeywords()).thenReturn(List.of("키워드")); diff --git a/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTestKt.kt b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTestKt.kt new file mode 100644 index 0000000..c7233d4 --- /dev/null +++ b/src/test/java/com/example/silverbridgeX_user/batch/tasklet/RequestUserEmbTaskletTestKt.kt @@ -0,0 +1,89 @@ +package com.example.silverbridgeX_user.batch.tasklet + +import com.example.silverbridgeX_user.global.util.EmbeddingClient +import com.example.silverbridgeX_user.user.domain.User +import com.example.silverbridgeX_user.user.repository.UserRepository +import com.example.silverbridgeX_user.user.service.UserService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.* +import org.springframework.batch.core.StepContribution +import org.springframework.batch.core.scope.context.ChunkContext +import org.springframework.batch.repeat.RepeatStatus + +class RequestUserEmbTaskletTestKt { + + private val userRepository: UserRepository = mock() + private val embeddingClient: EmbeddingClient = mock() + private val userService: UserService = mock() + private val contribution: StepContribution = mock() + private val chunkContext: ChunkContext = mock() + + private lateinit var tasklet: RequestUserEmbTasklet + + @BeforeEach + fun setUp() { + tasklet = RequestUserEmbTasklet(userRepository, embeddingClient, userService) + } + + @Test + fun `사용자의 키워드가 존재하면 임베딩이 갱신된다`() { + // Given + val user = mock { + on { id } doReturn 1L + on { preferredKeywords } doReturn listOf("문화", "자연") + } + + whenever(userRepository.findAll()).thenReturn(listOf(user)) + whenever(embeddingClient.getMaxPooledEmbeddingLiteral(any())) + .thenReturn("'[0.1, 0.2, 0.3]'") + + // When + val result = tasklet.execute(contribution, chunkContext) + + // Then + verify(userService).updatePreferredKeywords() + verify(userRepository).updatePreferredEmbedding(eq(1L), any()) + assertThat(result).isEqualTo(RepeatStatus.FINISHED) + } + + @Test + fun `사용자의 키워드가 없으면 임베딩 갱신을 건너뛴다`() { + // Given + val user = mock { + on { preferredKeywords } doReturn emptyList() + } + + whenever(userRepository.findAll()).thenReturn(listOf(user)) + + // When + val result = tasklet.execute(contribution, chunkContext) + + // Then + verify(userRepository, never()).updatePreferredEmbedding(any(), any()) + assertThat(result).isEqualTo(RepeatStatus.FINISHED) + } + + @Test + fun `임베딩 갱신 중 예외가 발생해도 정상 종료된다`() { + // Given + val user = mock { + on { id } doReturn 99L + on { preferredKeywords } doReturn listOf("키워드") + } + + whenever(userRepository.findAll()).thenReturn(listOf(user)) + whenever(embeddingClient.getMaxPooledEmbeddingLiteral(any())) + .thenReturn("'[0.1, 0.2]'") + doThrow(RuntimeException("DB Error")) + .whenever(userRepository).updatePreferredEmbedding(eq(99L), any()) + + // When + val result = tasklet.execute(contribution, chunkContext) + + // Then + verify(userRepository).updatePreferredEmbedding(eq(99L), any()) + assertThat(result).isEqualTo(RepeatStatus.FINISHED) + } +}