mirror of
https://github.com/cna-bootcamp/lifesub.git
synced 2026-06-13 04:59:10 +00:00
release
This commit is contained in:
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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
-10
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
+19
-12
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user