Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5a4eb77
feature: 관광지 데이터 api 호출 및 db 삽입
May 27, 2025
a6bbf52
refactor: 공통된 로직 따로 빼기,
May 27, 2025
02dcfb8
refactor: 가상 쓰레드 이용하여 각각의 활동 저장
May 27, 2025
973129c
core: 활동 설명 컬럼 길이 1000으로 높임
May 27, 2025
687565c
core: 활동 데이터 api 응답 dto
May 29, 2025
d3038a3
core: activity type enum에 tour_spot 추가
May 29, 2025
28aa954
refactor: 사용자 엔티티에 위도, 경도값 추가
May 29, 2025
dff4641
feature: 주소 사용해서 위도, 경도 좌표 계산 후 저장
May 29, 2025
41d0f46
core: 필요없어진 persona 엔티티 삭제
May 29, 2025
d8e7a88
refactor: user 엔티티에 선호/비선호 키워드와 임베딩 컬럼 추가
May 29, 2025
218bb2d
feature: 저장한 활동들 노드로 만들어 neo4j에 저장
May 29, 2025
f469108
fix: neo4j에서의 임베딩 값 저장 위해 type string -> float []로 변경
May 29, 2025
fc3720a
refactor: JPA가 임베딩 필드를 읽기 전용 필드로 취급하도록 변경
May 29, 2025
b742d44
refactor: JPA가 임베딩 필드를 읽기 전용 필드로 취급하도록 변경
May 29, 2025
3729e0c
refactor: username == email로 변경
May 29, 2025
1a09088
refactor: driver 선언부 변경
May 29, 2025
788d517
feature: 사용자별 추천 활동 반환 로직, 활동 로그 작성 로직
May 29, 2025
adb69c8
refactor: username == email
May 29, 2025
9228151
git pull
May 29, 2025
a84351b
refactor: user id를 string이 아닌 long으로 탐색
May 29, 2025
ef5e353
refactor: user 노드 생성시 id만 우선 저장
May 29, 2025
95120e5
refactor: 클릭수와 노출수 담당하는 컬럼 이름 변경
May 29, 2025
b3fd67d
refactor: 클릭수와 노출수 담당하는 컬럼 이름 변경
May 29, 2025
b2e439f
core: 활동 랭킹 저장 위한 테이블 추가
May 29, 2025
06cd49d
refactor: user 노드에 id만 속성으로 가지도록 수정
May 30, 2025
16f6578
refactor: @NoArgsConstructor 필요없기에 제거
Jun 5, 2025
ad144e0
feat: 메인 클래스에 Spring Batch 및 Scheduling 기능 활성화
Jun 12, 2025
f0cf450
feat: 선호 키워드 api로 불러와서 저장
Jun 12, 2025
cea1c72
feat: 선호 키워드 api로 불러와서 저장위한 dto
Jun 12, 2025
73ba648
feat: 추천 시스템 로직을 Spring Batch + Scheduler 기반으로 새벽 3시에 단계별 실행
Jun 12, 2025
d20cba7
feat: enum 컬럼 not null 제약 추가로 데이터 무결성 강화
Jun 12, 2025
0c59483
feat: 애플리케이션 실행 시마다 Spring Batch용 테이블들을 DROP 후 CREATE
Jun 12, 2025
450d02b
feat: @CreationTimestamp 추가하여 엔티티가 처음 저장(persist)될 때의 생성 시간 설정
Jun 12, 2025
f9d3eb3
refactor: 서버 name으로 수정
Jun 12, 2025
0eef9ca
refactor: 파라미터에 @NonNull 설정
Jun 12, 2025
ff61451
refactor: URL 환경변수 사용
Jun 12, 2025
7e66a08
bug fix: final 키워드로 PREF_URL을 필드 초기화하면서 chatServerUrl이 아직 null인 문제 해결
Jun 12, 2025
3138f5d
feat: 좌표값 얻는 api key 환경변수 등록
Jun 13, 2025
199ab57
feat: @Enumerated(EnumType.STRING) 설정
Jun 13, 2025
8eb921a
refactor: String인 ActionType에 맞게 쿼리 변경
Jun 13, 2025
9f323d5
test: spring batch 로직 테스트
Jun 13, 2025
94bb8d6
build: 테스팅에 kotlin 사용
Jun 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Expand All @@ -13,6 +14,10 @@ java {
}
}

kotlin {
jvmToolchain(21)
}

configurations {
compileOnly {
extendsFrom annotationProcessor
Expand All @@ -33,6 +38,10 @@ 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'
testImplementation 'org.springframework.batch:spring-batch-test'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -44,11 +53,17 @@ 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'
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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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<String> select(
@RequestParam(name = "activityId") Long activityId,
@GetMapping("/recommend-activity")
public ApiResponse<RecommendActivityResDtos> getRecommendActivities(
@AuthenticationPrincipal CustomUserDetails customUserDetails
) {
User user = userService.findByUserName(customUserDetails.getUsername());
recommendActivityService.handleActivitySelection(user, activityId);
return ApiResponse.onSuccess(SuccessCode.USER_LOGOUT_SUCCESS, "로그 저장 완료");
List<RecommendActivity> 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<String> view(
@RequestParam(name = "activityId") Long activityId,
@AuthenticationPrincipal CustomUserDetails customUserDetails
@PostMapping("/select")
public ApiResponse<String> 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<String> 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, "열람 로그 저장 완료");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 com.example.silverbridgeX_user.user.domain.User;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;

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 =
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<RecommendActivity> activities) {

List<RecommendActivityResDto> dtoList = activities.stream()
.map(RecommendActivityConverter::recommendActivityResDto)
.collect(Collectors.toList());

return RecommendActivityResDtos.builder()
.recommendActivityResDtos(dtoList)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.silverbridgeX_user.RecommendActivity.domain;

public enum ActionType {
VIEW,
SELECT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "activity_log")
public class ActivityLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ActionType actionType;

private LocalDateTime time;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "activity_id")
private Activity activity;

}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,6 +35,6 @@ public class RecommendActivity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "activity_id")
private User activity;
private Activity activity;

}
Original file line number Diff line number Diff line change
@@ -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<RecommendActivityResDto> 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;
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
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<RecommendActivity, Long> {
List<RecommendActivity> findAllByUser(User user);

void deleteByUser(User user);
}
Loading