mirror of
https://github.com/cna-bootcamp/lifesub.git
synced 2026-06-12 20:49:09 +00:00
release
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
com.unicorn.lifesub.recommend.RecommendApplication
|
||||
@@ -5,22 +5,31 @@ spring:
|
||||
application:
|
||||
name: recommend-service
|
||||
datasource:
|
||||
url: ${POSTGRES_URL}
|
||||
username: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:recommend}
|
||||
username: ${POSTGRES_USER:admin}
|
||||
password: ${POSTGRES_PASSWORD:Passw0rd}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
# JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
|
||||
allowedorigins: ${ALLOWED_ORIGINS:*}
|
||||
jwt:
|
||||
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
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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