This commit is contained in:
ondal
2025-02-13 00:16:35 +09:00
parent 2b100e1bcf
commit 11a49c33a8
101 changed files with 518 additions and 118 deletions
@@ -3,8 +3,12 @@ package com.unicorn.lifesub.recommend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@SpringBootApplication(
scanBasePackages = {
"com.unicorn.lifesub.recommend",
"com.unicorn.lifesub.common"
}
)
public class RecommendApplication {
public static void main(String[] args) {
SpringApplication.run(RecommendApplication.class, args);
@@ -0,0 +1,113 @@
// File: lifesub/recommend/src/main/java/com/unicorn/lifesub/recommend/config/DataLoader.java
package com.unicorn.lifesub.recommend.config;
import com.unicorn.lifesub.recommend.repository.entity.RecommendedCategoryEntity;
import com.unicorn.lifesub.recommend.repository.entity.SpendingEntity;
import com.unicorn.lifesub.recommend.repository.jpa.RecommendRepository;
import com.unicorn.lifesub.recommend.repository.jpa.SpendingRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@Slf4j
@Component
@RequiredArgsConstructor
public class DataLoader implements CommandLineRunner {
private final RecommendRepository recommendRepository;
private final SpendingRepository spendingRepository;
private final Random random = new Random();
private static final List<String> SPENDING_CATEGORIES = Arrays.asList(
"COSMETICS", "ENTERTAINMENT", "EDUCATION", "RESTAURANT", "MUSIC", "DAILY"
);
private static final List<String> SUBSCRIPTION_CATEGORIES = Arrays.asList(
"BEAUTY", "OTT", "EDU", "FOOD", "MUSIC", "LIFE"
);
@Override // CommandLineRunner의 run 메소드 구현
@Transactional
public void run(String... args) throws Exception {
log.info("Initializing sample data...");
initSpendingHistory();
initRecommendedCategories();
log.info("Sample data initialization completed.");
}
private void initSpendingHistory() {
// 기존 데이터 삭제
spendingRepository.deleteAll();
List<SpendingEntity> spendings = new ArrayList<>();
LocalDate now = LocalDate.now();
// user01 ~ user10까지의 지출 데이터 생성
for (int i = 1; i <= 10; i++) {
String userId = String.format("user%02d", i);
// 각 사용자별로 지난 한달간 5~10건의 지출 데이터 생성
int numTransactions = 5 + random.nextInt(6);
for (int j = 0; j < numTransactions; j++) {
String category = SPENDING_CATEGORIES.get(random.nextInt(SPENDING_CATEGORIES.size()));
long amount = (50 + random.nextInt(451)) * 1000L; // 5만원 ~ 50만원 사이
int daysAgo = random.nextInt(30); // 최근 30일 이내
spendings.add(SpendingEntity.builder()
.userId(userId)
.category(category)
.amount(amount)
.spendingDate(now.minusDays(daysAgo))
.build());
}
}
spendingRepository.saveAll(spendings);
log.info("Spending history data initialized with {} records", spendings.size());
}
private void initRecommendedCategories() {
// 기존 데이터 삭제
recommendRepository.deleteAll();
// 지출 카테고리와 추천 구독 카테고리 매핑 데이터 생성
List<RecommendedCategoryEntity> categories = Arrays.asList(
RecommendedCategoryEntity.builder()
.spendingCategory("COSMETICS")
.recommendCategory("BEAUTY")
.build(),
RecommendedCategoryEntity.builder()
.spendingCategory("ENTERTAINMENT")
.recommendCategory("OTT")
.build(),
RecommendedCategoryEntity.builder()
.spendingCategory("EDUCATION")
.recommendCategory("EDU")
.build(),
RecommendedCategoryEntity.builder()
.spendingCategory("RESTAURANT")
.recommendCategory("FOOD")
.build(),
RecommendedCategoryEntity.builder()
.spendingCategory("MUSIC")
.recommendCategory("MUSIC")
.build(),
RecommendedCategoryEntity.builder()
.spendingCategory("DAILY")
.recommendCategory("LIFE")
.build()
);
recommendRepository.saveAll(categories);
log.info("Recommended categories data initialized with {} records", categories.size());
}
}
@@ -24,7 +24,7 @@ import java.util.List;
public class SecurityConfig {
protected final JwtTokenProvider jwtTokenProvider;
@Value("${allowedorigins}")
@Value("${allowed-origins}")
private String allowedOrigins;
public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
@@ -25,7 +25,7 @@ import java.util.stream.Collectors;
public class JwtTokenProvider {
private final Algorithm algorithm;
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
public JwtTokenProvider(@Value("${jwt.secret-key}") String secretKey) {
this.algorithm = Algorithm.HMAC512(secretKey);
}
@@ -4,6 +4,7 @@ import com.unicorn.lifesub.common.dto.ApiResponse;
import com.unicorn.lifesub.recommend.dto.RecommendCategoryDTO;
import com.unicorn.lifesub.recommend.service.RecommendService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
@Tag(name = "구독추천 API", description = "구독추천 관련 API")
@RestController
@SecurityRequirement(name = "bearerAuth") //이 어노테이션이 없으면 요청 헤더에 Authorization헤더가 안 생김
@RequestMapping("/api/recommend")
@RequiredArgsConstructor
public class RecommendController {
@@ -8,6 +8,7 @@ import java.time.LocalDate;
@Builder
public class RecommendCategoryDTO {
private String categoryName;
private String imagePath;
private LocalDate baseDate;
private String spendingCategory;
private Long totalSpending;
}
@@ -1,19 +1,10 @@
package com.unicorn.lifesub.recommend.repository.jpa;
import com.unicorn.lifesub.recommend.repository.entity.RecommendedCategoryEntity;
import com.unicorn.lifesub.recommend.repository.entity.SpendingEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface RecommendRepository extends JpaRepository<RecommendedCategoryEntity, Long> {
Optional<RecommendedCategoryEntity> findBySpendingCategory(String category);
@Query("SELECT s FROM SpendingEntity s WHERE s.userId = :userId AND s.spendingDate >= :startDate")
List<SpendingEntity> findSpendingsByUserIdAndDateAfter(@Param("userId") String userId,
@Param("startDate") LocalDate startDate);
}
}
@@ -0,0 +1,16 @@
// File: lifesub/recommend/src/main/java/com/unicorn/lifesub/recommend/repository/jpa/SpendingRepository.java
package com.unicorn.lifesub.recommend.repository.jpa;
import com.unicorn.lifesub.recommend.repository.entity.SpendingEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
import java.util.List;
public interface SpendingRepository extends JpaRepository<SpendingEntity, Long> {
@Query("SELECT s FROM SpendingEntity s WHERE s.userId = :userId AND s.spendingDate >= :startDate")
List<SpendingEntity> findSpendingsByUserIdAndDateAfter(@Param("userId") String userId,
@Param("startDate") LocalDate startDate);
}
@@ -1,3 +1,4 @@
// File: lifesub/recommend/src/main/java/com/unicorn/lifesub/recommend/service/RecommendServiceImpl.java
package com.unicorn.lifesub.recommend.service;
import com.unicorn.lifesub.common.exception.BusinessException;
@@ -6,44 +7,50 @@ import com.unicorn.lifesub.recommend.domain.RecommendedCategory;
import com.unicorn.lifesub.recommend.domain.SpendingCategory;
import com.unicorn.lifesub.recommend.dto.RecommendCategoryDTO;
import com.unicorn.lifesub.recommend.repository.jpa.RecommendRepository;
import com.unicorn.lifesub.recommend.repository.jpa.SpendingRepository;
import com.unicorn.lifesub.recommend.repository.entity.SpendingEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
@Service
@RequiredArgsConstructor
public class RecommendServiceImpl implements RecommendService {
private final RecommendRepository recommendRepository;
private final SpendingRepository spendingRepository;
private final SpendingAnalyzer spendingAnalyzer;
@Override
@Transactional(readOnly = true)
public RecommendCategoryDTO getRecommendedCategory(String userId) {
LocalDate startDate = LocalDate.now().minusMonths(1);
SpendingCategory topSpending = spendingAnalyzer.analyzeSpending(
recommendRepository.findSpendingsByUserIdAndDateAfter(userId, startDate)
);
List<SpendingEntity> spendings = spendingRepository.findSpendingsByUserIdAndDateAfter(userId, startDate);
if (spendings.isEmpty()) {
throw new BusinessException(ErrorCode.NO_SPENDING_DATA);
}
SpendingCategory topSpending = spendingAnalyzer.analyzeSpending(spendings);
if (topSpending == null) {
throw new BusinessException(ErrorCode.NO_SPENDING_DATA);
}
RecommendedCategory recommendedCategory = recommendRepository
.findBySpendingCategory(topSpending.getCategory())
.orElseThrow(() -> new BusinessException(ErrorCode.NO_SPENDING_DATA))
.toDomain();
.findBySpendingCategory(topSpending.getCategory())
.orElseThrow(() -> new BusinessException(ErrorCode.NO_RECOMMENDATION_DATA))
.toDomain();
return RecommendCategoryDTO.builder()
.categoryName(recommendedCategory.getRecommendCategory())
.imagePath(getCategoryImagePath(recommendedCategory.getRecommendCategory()))
.baseDate(recommendedCategory.getBaseDate())
.spendingCategory(topSpending.getCategory())
.totalSpending(topSpending.getTotalAmount())
.build();
}
private String getCategoryImagePath(String category) {
return "/images/categories/" + category.toLowerCase() + ".png";
}
}
}
+12 -6
View File
@@ -9,21 +9,27 @@ spring:
username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:Passw0rd}
driver-class-name: org.postgresql.Driver
# JPA 설정
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false}
show-sql: ${JPA_SHOW_SQL:true}
properties:
hibernate:
format_sql: true
allowedorigins: ${ALLOWED_ORIGINS:*}
dialect: org.hibernate.dialect.PostgreSQLDialect
jwt:
secret: ${JWT_SECRET:8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ==}
secret-key: ${JWT_SECRET_KEY:8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ}
allowed-origins: ${ALLOWED_ORIGINS:*}
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /api-docs
logging:
level:
com.unicorn: DEBUG
org.hibernate.SQL: TRACE