diff --git a/build.gradle b/build.gradle index 5ae542b7..09de3c1a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,67 +1,75 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.2.7' - id 'io.spring.dependency-management' version '1.1.5' - id 'com.google.cloud.tools.jib' version '3.4.3' + id 'java' + id 'org.springframework.boot' version '3.2.7' + id 'io.spring.dependency-management' version '1.1.5' + id 'com.google.cloud.tools.jib' version '3.4.3' } group = 'com.bbteam' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-security' // security - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' //Swagger + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' // security + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' //Swagger - implementation 'net.nurigo:sdk:4.2.7' // 문자메시지 대행 서비스 + implementation 'net.nurigo:sdk:4.2.7' // 문자메시지 대행 서비스 - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-cache' // cache + + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') // jackson으로 jwt 파싱 + + // querydsl 관련 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } jib { - from { - image = 'openjdk:17-alpine' - platforms { - platform { - architecture = 'amd64' - os = 'linux' - } - } - } - to { - image = 'binjumeoniz1/binjumeoniz:latest' - } - container { - jvmFlags = ['-Dspring.profiles.active=dev'] - } + from { + image = 'openjdk:17-alpine' + platforms { + platform { + architecture = 'amd64' + os = 'linux' + } + } + } + to { + image = 'binjumeoniz1/binjumeoniz:latest' + } + container { + jvmFlags = ['-Dspring.profiles.active=dev'] + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/BudgetbuddiesApplication.java b/src/main/java/com/bbteam/budgetbuddies/BudgetbuddiesApplication.java index 42ea116f..8563c078 100644 --- a/src/main/java/com/bbteam/budgetbuddies/BudgetbuddiesApplication.java +++ b/src/main/java/com/bbteam/budgetbuddies/BudgetbuddiesApplication.java @@ -2,10 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableJpaAuditing +@EnableAsync +@EnableCaching public class BudgetbuddiesApplication { public static void main(String[] args) { diff --git a/src/main/java/com/bbteam/budgetbuddies/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/bbteam/budgetbuddies/apiPayload/code/status/ErrorStatus.java index 8c9515b8..b50a6ca4 100644 --- a/src/main/java/com/bbteam/budgetbuddies/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/bbteam/budgetbuddies/apiPayload/code/status/ErrorStatus.java @@ -11,7 +11,7 @@ public enum ErrorStatus implements BaseErrorCode { - _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5000", "서버에러"), + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5000", "서버 에러. 관리자에게 문의하세요."), _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON4000", "잘못된 요청"), _USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자가 없습니다."), _COMMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "COMMENT4001", "해당 댓글이 없습니다.") , @@ -27,7 +27,9 @@ public enum ErrorStatus implements BaseErrorCode { _OTP_NOT_VALID(HttpStatus.BAD_REQUEST, "OTP4001", "인증번호가 유효하지 않습니다."), _PHONE_NUMBER_NOT_VALID(HttpStatus.BAD_REQUEST, "AUTH4001", "전화번호 형식이 유효하지 않습니다. (예: 01012341234)"), - _FAQ_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ4004", "해당 FAQ가 존재하지 않습니다."); + _FAQ_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ4004", "해당 FAQ가 존재하지 않습니다."), + _SEARCH_KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "SEARCH_KEYWORD4004", "해당 SearchKeyword가 존재하지 않습니다."), + _FAQ_KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ_KEYWORD4004", "해당 FaqKeyword가 존재하지 않습니다."); private HttpStatus httpStatus; diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryService.java b/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryService.java index 92083193..0a75b0c9 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryService.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryService.java @@ -14,8 +14,10 @@ public interface CategoryService { List getUserCategories(Long userId); - Category handleCategoryChange(Expense expense, ExpenseUpdateRequestDto request, User user); - void deleteCategory(Long id, Long userId); + + Category getCategory(Long categoryId); + + List getUserCategoryList(Long userId); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryServiceImpl.java index af6ae372..7a010856 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/category/service/CategoryServiceImpl.java @@ -5,7 +5,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,9 +15,8 @@ import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; import com.bbteam.budgetbuddies.domain.consumptiongoal.entity.ConsumptionGoal; import com.bbteam.budgetbuddies.domain.consumptiongoal.repository.ConsumptionGoalRepository; -import com.bbteam.budgetbuddies.domain.consumptiongoal.service.ConsumptionGoalService; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; import com.bbteam.budgetbuddies.domain.expense.entity.Expense; +import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; @@ -30,7 +28,6 @@ public class CategoryServiceImpl implements CategoryService { private final CategoryRepository categoryRepository; - private final ConsumptionGoalService consumptionGoalService; private final UserRepository userRepository; private final CategoryConverter categoryConverter; private final ConsumptionGoalRepository consumptionGoalRepository; @@ -40,11 +37,11 @@ public class CategoryServiceImpl implements CategoryService { @Transactional public CategoryResponseDto createCategory(Long userId, CategoryRequestDto categoryRequestDto) { User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); + .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); // 동일한 이름의 삭제된 카테고리가 존재하는지 확인 Optional existingCategory = categoryRepository.findByNameAndUserIdAndDeletedTrue( - categoryRequestDto.getName(), userId); + categoryRequestDto.getName(), userId); if (existingCategory.isPresent()) { // 삭제된 카테고리가 존재하면 복구 (deleted = false) @@ -54,7 +51,7 @@ public CategoryResponseDto createCategory(Long userId, CategoryRequestDto catego // 해당 카테고리의 삭제된 ConsumptionGoal도 복구 Optional existingConsumptionGoal = consumptionGoalRepository.findByUserAndCategoryAndDeletedTrue( - user, categoryToRestore); + user, categoryToRestore); if (existingConsumptionGoal.isPresent()) { ConsumptionGoal consumptionGoalToRestore = existingConsumptionGoal.get(); @@ -65,13 +62,13 @@ public CategoryResponseDto createCategory(Long userId, CategoryRequestDto catego } else { // ConsumptionGoal이 존재하지 않으면 새로 생성 ConsumptionGoal newConsumptionGoal = ConsumptionGoal.builder() - .user(user) - .category(categoryToRestore) - .goalMonth(LocalDate.now().withDayOfMonth(1)) // 현재 달로 목표 설정 - .consumeAmount(0L) - .goalAmount(0L) - .deleted(false) // 생성할 때 삭제 상태가 아니도록 - .build(); + .user(user) + .category(categoryToRestore) + .goalMonth(LocalDate.now().withDayOfMonth(1)) // 현재 달로 목표 설정 + .consumeAmount(0L) + .goalAmount(0L) + .deleted(false) // 생성할 때 삭제 상태가 아니도록 + .build(); consumptionGoalRepository.save(newConsumptionGoal); } @@ -83,13 +80,13 @@ public CategoryResponseDto createCategory(Long userId, CategoryRequestDto catego // 새로운 카테고리에 대한 ConsumptionGoal도 생성 ConsumptionGoal newConsumptionGoal = ConsumptionGoal.builder() - .user(user) - .category(newCategory) - .goalMonth(LocalDate.now().withDayOfMonth(1)) // 현재 달로 목표 설정 - .consumeAmount(0L) - .goalAmount(0L) - .deleted(false) // 생성할 때 삭제 상태가 아니도록 - .build(); + .user(user) + .category(newCategory) + .goalMonth(LocalDate.now().withDayOfMonth(1)) // 현재 달로 목표 설정 + .consumeAmount(0L) + .goalAmount(0L) + .deleted(false) // 생성할 때 삭제 상태가 아니도록 + .build(); consumptionGoalRepository.save(newConsumptionGoal); return categoryConverter.toCategoryResponseDto(newCategory); @@ -100,21 +97,15 @@ public CategoryResponseDto createCategory(Long userId, CategoryRequestDto catego public List getUserCategories(Long userId) { userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId)); + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId)); List categories = categoryRepository.findUserCategoryByUserId(userId); return categories.stream().map(categoryConverter::toCategoryResponseDto).collect(Collectors.toList()); } @Override - @Transactional(readOnly = true) - public Category handleCategoryChange(Expense expense, ExpenseUpdateRequestDto request, User user) { - Category categoryToReplace = categoryRepository.findById(request.getCategoryId()) - .orElseThrow(() -> new IllegalArgumentException("Not found category")); - - consumptionGoalService.recalculateConsumptionAmount(expense, request, user); - - return categoryToReplace; + public Category getCategory(Long id) { + return categoryRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Not found category")); } @Override @@ -131,15 +122,15 @@ public void deleteCategory(Long categoryId, Long userId) { // 현재 월에 해당하는 삭제되지 않은 Expense 조회 (deleted = false) List currentMonthExpenses = expenseRepository.findByCategoryIdAndUserIdAndExpenseDateBetweenAndDeletedFalse( - categoryId, userId, startOfMonth.atStartOfDay(), endOfMonth.atTime(23, 59, 59)); + categoryId, userId, startOfMonth.atStartOfDay(), endOfMonth.atTime(23, 59, 59)); long totalAmount = currentMonthExpenses.stream() - .mapToLong(Expense::getAmount) - .sum(); + .mapToLong(Expense::getAmount) + .sum(); // category_id = 10(기타 카테고리)의 소비 목표 업데이트 (custom 카테고리 삭제로 인한 소비 내역은 삭제되지 않고 기타 카테고리로..) ConsumptionGoal goal = consumptionGoalRepository.findByCategoryIdAndUserId(10L, userId) - .orElseThrow(() -> new IllegalArgumentException("No consumption goal found for category_id 10.")); + .orElseThrow(() -> new IllegalArgumentException("No consumption goal found for category_id 10.")); goal.setConsumeAmount(goal.getConsumeAmount() + totalAmount); consumptionGoalRepository.save(goal); @@ -161,4 +152,8 @@ public void deleteCategory(Long categoryId, Long userId) { }); } + @Override + public List getUserCategoryList(Long userId) { + return categoryRepository.findUserCategoryByUserId(userId); + } } \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalApi.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalApi.java index 69094dbc..304fd7c5 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalApi.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalApi.java @@ -2,18 +2,20 @@ import java.time.LocalDate; import java.util.List; +import java.util.concurrent.ExecutionException; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; import com.bbteam.budgetbuddies.apiPayload.ApiResponse; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.AllConsumptionCategoryResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionAnalysisResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalListRequestDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseListDto; +import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.MonthReportResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.PeerInfoResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopCategoryConsumptionDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopGoalCategoryResponseDto; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -25,62 +27,75 @@ public interface ConsumptionGoalApi { @Operation(summary = "[User] 또래들이 가장 큰 계획을 세운 카테고리 조회 Top4", description = "특정 사용자의 소비 목표 카테고리별 소비 목표 금액을 조회하는 API 입니다.") @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) - @Parameters({@Parameter(name = "userId", description = "로그인 한 유저 아이디"), + @Parameters({ @Parameter(name = "peerAgeStart", description = "또래나이 시작 범위"), @Parameter(name = "peerAgeEnd", description = "또래나이 끝 범위"), @Parameter(name = "peerGender", description = "또래 성별")}) ApiResponse> getTopConsumptionGoalCategories( - Long userId, int peerAgeStart, int peerAgeEnd, String peerGender); + UserDto.AuthUserDto user, int peerAgeStart, int peerAgeEnd, String peerGender); @Operation(summary = "[User] 또래들이 가장 많이 계획한 카테고리와 평균 금액 및 내 목표금액 차이 조회", description = "특정 사용자의 또래 소비 카테고리별 평균 목표 금액을 조회하는 API 입니다.") @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) - @Parameters({@Parameter(name = "userId", description = "로그인 한 유저 아이디"), + @Parameters({ @Parameter(name = "peerAgeStart", description = "또래나이 시작 범위"), @Parameter(name = "peerAgeEnd", description = "또래나이 끝 범위"), @Parameter(name = "peerGender", description = "또래 성별")}) - ApiResponse> getAllConsumptionGoalCategories(Long userId, int peerAgeStart, + ApiResponse> getAllConsumptionGoalCategories(UserDto.AuthUserDto user, + int peerAgeStart, int peerAgeEnd, String peerGender); @Operation(summary = "[User] 또래나이와 성별 조회", description = "또래나이와 성별을 조회하는 API 입니다.") @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) - @Parameters({@Parameter(name = "userId", description = "로그인 한 유저 아이디"), + @Parameters({ @Parameter(name = "peerAgeStart", description = "또래나이 시작 범위"), @Parameter(name = "peerAgeEnd", description = "또래나이 끝 범위"), @Parameter(name = "peerGender", description = "또래 성별")}) - ApiResponse getPeerInfo(Long userId, int peerAgeStart, int peerAgeEnd, String peerGender); + ApiResponse getPeerInfo(UserDto.AuthUserDto user, int peerAgeStart, int peerAgeEnd, + String peerGender); @Operation(summary = "[User] 소비 목표 조회", description = "date={yyyy-MM-dd} 형식의 query string을 통해서 사용자의 목표 달을 조회하는 API 입니다.") @Parameters({@Parameter(name = "date", description = "yyyy-MM-dd 형식으로 목표 달의 소비를 조회")}) - ApiResponse findUserConsumptionGoal(LocalDate date, Long userId); + ApiResponse findUserConsumptionGoal(LocalDate date, UserDto.AuthUserDto user); @Operation(summary = "[User] 이번 달 소비 목표 수정", description = "다른 달의 소비 목표를 업데이트하는 것은 불가능하고 오직 이번 달의 소비 목표만 업데이트 하는 API 입니다.") - ResponseEntity updateOrElseGenerateConsumptionGoal(Long userId, + ResponseEntity updateOrElseGenerateConsumptionGoal(UserDto.AuthUserDto user, ConsumptionGoalListRequestDto consumptionGoalListRequestDto); @Operation(summary = "[User] 또래들이 가장 많이한 소비 카테고리 조회 Top3", description = "특정 사용자의 또래 소비 카테고리별 소비 건 수을 조회하는 API 입니다.") @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) - @Parameters({@Parameter(name = "userId", description = "로그인 한 유저 아이디"), + @Parameters({ @Parameter(name = "peerAgeStart", description = "또래나이 시작 범위"), @Parameter(name = "peerAgeEnd", description = "또래나이 끝 범위"), @Parameter(name = "peerGender", description = "또래 성별")}) - ApiResponse> getTopConsumptionCategories(Long userId, int peerAgeStart, + ApiResponse> getTopConsumptionCategories(UserDto.AuthUserDto user, int peerAgeStart, int peerAgeEnd, String peerGender); @Operation(summary = "[User] 또래들이 가장 많이한 소비 카테고리와 평균 금액 및 내 소비금액 차이 조회", description = "특정 사용자의 또래 소비 카테고리별 평균 소비 금액을 조회하는 API 입니다.") @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) - @Parameters({@Parameter(name = "userId", description = "로그인 한 유저 아이디"), + @Parameters({ @Parameter(name = "peerAgeStart", description = "또래나이 시작 범위"), @Parameter(name = "peerAgeEnd", description = "또래나이 끝 범위"), @Parameter(name = "peerGender", description = "또래 성별")}) - ApiResponse> getAllConsumptionCategories(Long userId, int peerAgeStart, + ApiResponse> getAllConsumptionCategories(UserDto.AuthUserDto user, + int peerAgeStart, int peerAgeEnd, String peerGender); @Operation(summary = "[User] 또래들이 가장 큰 목표로 세운 카테고리와 그 카테고리에서 이번주 사용한 금액 조회", description = "특정 사용자의 또래 소비 카테고리별 이번주 소비 금액을 조회하는 API 입니다.") @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) - ApiResponse getTopCategoryAndConsumptionAmount(@PathVariable Long userId); + ApiResponse getTopCategoryAndConsumptionAmount(UserDto.AuthUserDto user); + + @Operation(summary = "[User] 이번 달 레포트 표정, 멘트 조회 ", description = "특정 사용자의 이번 달 레포트 표정, 멘트를 조회하는 API 입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) + ApiResponse getMonthReport(UserDto.AuthUserDto user); + + @Operation(summary = "[User] 소비 분석 멘트 생성", description = "특정 사용자의 소비 분석 멘트를 생성 하는 API 입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공")}) + ApiResponse getConsumptionMention(UserDto.AuthUserDto user) throws ExecutionException, InterruptedException; } \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalController.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalController.java index 4a75809d..f8ee55bf 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalController.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/controller/ConsumptionGoalController.java @@ -2,6 +2,8 @@ import java.time.LocalDate; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; @@ -23,6 +25,8 @@ import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopCategoryConsumptionDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopGoalCategoryResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.service.ConsumptionGoalService; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; +import com.bbteam.budgetbuddies.global.security.utils.AuthUser; import lombok.RequiredArgsConstructor; @@ -36,93 +40,104 @@ public class ConsumptionGoalController implements ConsumptionGoalApi { @Override @GetMapping("/categories/top-goals/top-4") public ApiResponse> getTopConsumptionGoalCategories( - @RequestParam(name = "userId") Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(name = "peerAgeStart", defaultValue = "0") int peerAgeStart, @RequestParam(name = "peerAgeEnd", defaultValue = "0") int peerAgeEnd, @RequestParam(name = "peerGender", defaultValue = "none") String peerGender) { List response = consumptionGoalService.getTopConsumptionGoalCategories( - userId, peerAgeStart, peerAgeEnd, peerGender); + user.getId(), peerAgeStart, peerAgeEnd, peerGender); return ApiResponse.onSuccess(response); } + @Override @GetMapping("/categories/top-goals") public ApiResponse> getAllConsumptionGoalCategories( - @RequestParam(name = "userId") Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(name = "peerAgeStart", defaultValue = "0") int peerAgeStart, @RequestParam(name = "peerAgeEnd", defaultValue = "0") int peerAgeEnd, @RequestParam(name = "peerGender", defaultValue = "none") String peerGender) { List response = consumptionGoalService.getAllConsumptionGoalCategories( - userId, + user.getId(), peerAgeStart, peerAgeEnd, peerGender); return ApiResponse.onSuccess(response); } @Override @GetMapping("/peer-info") - public ApiResponse getPeerInfo(@RequestParam(name = "userId") Long userId, + public ApiResponse getPeerInfo( + @AuthUser UserDto.AuthUserDto user, @RequestParam(name = "peerAgeStart", defaultValue = "0") int peerAgeStart, @RequestParam(name = "peerAgeEnd", defaultValue = "0") int peerAgeEnd, @RequestParam(name = "peerGender", defaultValue = "none") String peerGender) { - PeerInfoResponseDto response = consumptionGoalService.getPeerInfo(userId, peerAgeStart, peerAgeEnd, peerGender); + PeerInfoResponseDto response = consumptionGoalService.getPeerInfo(user.getId(), peerAgeStart, peerAgeEnd, + peerGender); return ApiResponse.onSuccess(response); } - @GetMapping("/{userId}") + @GetMapping() public ApiResponse findUserConsumptionGoal( - @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date, @PathVariable Long userId) { - - ConsumptionGoalResponseListDto response = consumptionGoalService.findUserConsumptionGoalList(userId, date); + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date, @AuthUser UserDto.AuthUserDto user) { - return ApiResponse.onSuccess(response); + return ApiResponse.onSuccess(consumptionGoalService.findUserConsumptionGoalList(user.getId(), date)); } @Override - @PostMapping("/{userId}") - public ResponseEntity updateOrElseGenerateConsumptionGoal(@PathVariable Long userId, + @PostMapping() + public ResponseEntity updateOrElseGenerateConsumptionGoal( + @AuthUser UserDto.AuthUserDto user, @RequestBody ConsumptionGoalListRequestDto consumptionGoalListRequestDto) { return ResponseEntity.ok() - .body(consumptionGoalService.updateConsumptionGoals(userId, consumptionGoalListRequestDto)); + .body(consumptionGoalService.updateConsumptionGoals(user.getId(), consumptionGoalListRequestDto)); } + @Override @GetMapping("/categories/top-consumptions/top-3") public ApiResponse> getTopConsumptionCategories( - @RequestParam(name = "userId") Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(name = "peerAgeStart", defaultValue = "0") int peerAgeStart, @RequestParam(name = "peerAgeEnd", defaultValue = "0") int peerAgeEnd, @RequestParam(name = "peerGender", defaultValue = "none") String peerGender) { - List response = consumptionGoalService.getTopConsumptionCategories(userId, + List response = consumptionGoalService.getTopConsumptionCategories(user.getId(), peerAgeStart, peerAgeEnd, peerGender); return ApiResponse.onSuccess(response); } + @Override @GetMapping("/categories/top-consumptions") public ApiResponse> getAllConsumptionCategories( - @RequestParam(name = "userId") Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(name = "peerAgeStart", defaultValue = "0") int peerAgeStart, @RequestParam(name = "peerAgeEnd", defaultValue = "0") int peerAgeEnd, @RequestParam(name = "peerGender", defaultValue = "none") String peerGender) { - List response = consumptionGoalService.getAllConsumptionCategories(userId, + List response = consumptionGoalService.getAllConsumptionCategories( + user.getId(), peerAgeStart, peerAgeEnd, peerGender); return ApiResponse.onSuccess(response); } + @Override @GetMapping("/category/top-goals") public ApiResponse getTopCategoryAndConsumptionAmount( - @RequestParam(name = "userId") Long userId) { - ConsumptionAnalysisResponseDto response = consumptionGoalService.getTopCategoryAndConsumptionAmount(userId); + @AuthUser UserDto.AuthUserDto user) { + ConsumptionAnalysisResponseDto response = consumptionGoalService.getTopCategoryAndConsumptionAmount( + user.getId()); return ApiResponse.onSuccess(response); } - @GetMapping("/facialExpressions") - public ApiResponse getMonthReport(@RequestParam(name = "userId") Long userId) { - MonthReportResponseDto response = consumptionGoalService.getMonthReport(userId); + @Override + @GetMapping("/month-report") + public ApiResponse getMonthReport( + @AuthUser UserDto.AuthUserDto user) { + MonthReportResponseDto response = consumptionGoalService.getMonthReport(user.getId()); return ApiResponse.onSuccess(response); } - @GetMapping("/cosnumption-ment") - public ApiResponse getConsumptionMention(@RequestParam(name = "userId") Long userId) { - String response = consumptionGoalService.getConsumptionMention(userId); - return ApiResponse.onSuccess(response); + @Override + @GetMapping("/consumption-ment") + public ApiResponse getConsumptionMention( + @AuthUser UserDto.AuthUserDto user) throws ExecutionException, InterruptedException { + CompletableFuture response = consumptionGoalService.getConsumptionMention(user.getId()); + return ApiResponse.onSuccess(response.get()); } } \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java index 5cd7a72a..44c0dd08 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java @@ -1,11 +1,11 @@ package com.bbteam.budgetbuddies.domain.consumptiongoal.converter; import java.time.LocalDate; +import java.util.Comparator; import java.util.List; import org.springframework.stereotype.Component; -import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionAnalysisResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseListDto; @@ -17,15 +17,6 @@ @Component public class ConsumptionGoalConverter { - public ConsumptionGoalResponseDto toConsumptionGoalResponseDto(Category category) { - return ConsumptionGoalResponseDto.builder() - .categoryName(category.getName()) - .categoryId(category.getId()) - .goalAmount(0L) - .consumeAmount(0L) - .build(); - } - public ConsumptionGoalResponseDto toConsumptionGoalResponseDto(ConsumptionGoal consumptionGoal) { return ConsumptionGoalResponseDto.builder() .categoryName(consumptionGoal.getCategory().getName()) @@ -35,27 +26,24 @@ public ConsumptionGoalResponseDto toConsumptionGoalResponseDto(ConsumptionGoal c .build(); } - public ConsumptionGoalResponseDto toConsumptionGoalResponseDtoFromPreviousGoal(ConsumptionGoal consumptionGoal) { - return ConsumptionGoalResponseDto.builder() - .categoryName(consumptionGoal.getCategory().getName()) - .categoryId(consumptionGoal.getCategory().getId()) - .goalAmount(consumptionGoal.getGoalAmount()) - .consumeAmount(0L) - .build(); + public ConsumptionGoalResponseListDto toConsumptionGoalResponseListDto( + List consumptionGoalList, LocalDate goalMonth) { - } + List consumptionGoalResponseList = consumptionGoalList + .stream() + .map(this::toConsumptionGoalResponseDto) + .sorted(Comparator.comparingLong(ConsumptionGoalResponseDto::getRemainingBalance).reversed()) + .toList(); - public ConsumptionGoalResponseListDto toConsumptionGoalResponseListDto( - List consumptionGoalList, LocalDate goalMonth) { - Long totalGoalAmount = sumTotalGoalAmount(consumptionGoalList); - Long totalConsumptionAmount = sumTotalConsumptionAmount(consumptionGoalList); + Long totalGoalAmount = sumTotalGoalAmount(consumptionGoalResponseList); + Long totalConsumptionAmount = sumTotalConsumptionAmount(consumptionGoalResponseList); return ConsumptionGoalResponseListDto.builder() .goalMonth(goalMonth) .totalGoalAmount(totalGoalAmount) .totalConsumptionAmount(totalConsumptionAmount) .totalRemainingBalance(totalGoalAmount - totalConsumptionAmount) - .consumptionGoalList(consumptionGoalList) + .consumptionGoalList(consumptionGoalResponseList) .build(); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/entity/ConsumptionGoal.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/entity/ConsumptionGoal.java index 71d2accc..229f2df3 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/entity/ConsumptionGoal.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/entity/ConsumptionGoal.java @@ -48,7 +48,7 @@ public class ConsumptionGoal extends BaseEntity { @JoinColumn(name = "category_id") private Category category; - public void updateConsumeAmount(Long amount) { + public void addConsumeAmount(Long amount) { this.consumeAmount += amount; } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepository.java index ad52c9c8..72284b9b 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepository.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepository.java @@ -25,8 +25,7 @@ public interface ConsumptionGoalRepository extends JpaRepository findConsumptionGoalByUserIdAndGoalMonth(Long userId, LocalDate goalMonth); - Optional findConsumptionGoalByUserAndCategoryAndGoalMonth(User user, Category category, - LocalDate goalMonth); + Optional findByUserAndCategoryAndGoalMonth(User user, Category category, LocalDate goalMonth); @Query("SELECT AVG(cg.consumeAmount) FROM ConsumptionGoal cg " + "JOIN cg.category c " + diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalService.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalService.java index de727f3d..18e6948e 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalService.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalService.java @@ -2,9 +2,11 @@ import java.time.LocalDate; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.stereotype.Service; +import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.AllConsumptionCategoryResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionAnalysisResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalListRequestDto; @@ -13,8 +15,7 @@ import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.PeerInfoResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopCategoryConsumptionDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopGoalCategoryResponseDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; -import com.bbteam.budgetbuddies.domain.expense.entity.Expense; +import com.bbteam.budgetbuddies.domain.consumptiongoal.entity.ConsumptionGoal; import com.bbteam.budgetbuddies.domain.user.entity.User; @Service @@ -35,7 +36,8 @@ ConsumptionGoalResponseListDto updateConsumptionGoals(Long userId, ConsumptionAnalysisResponseDto getTopCategoryAndConsumptionAmount(Long userId); - void recalculateConsumptionAmount(Expense expense, ExpenseUpdateRequestDto request, User user); + void recalculateConsumptionAmount(ConsumptionGoal beforeConsumptionGoal, Long beforeAmount, + ConsumptionGoal afterConsumptionGoal, Long afterAmount); void updateConsumeAmount(Long userId, Long categoryId, Long amount); @@ -51,5 +53,7 @@ List getAllConsumptionCategories(Long userId, MonthReportResponseDto getMonthReport(Long userId); - String getConsumptionMention(Long userId); + CompletableFuture getConsumptionMention(Long userId); + + ConsumptionGoal getUserConsumptionGoal(User user, Category category, LocalDate goalDate); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java index 73e1ba13..24e8e4c5 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java @@ -17,14 +17,18 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; +import com.bbteam.budgetbuddies.domain.category.service.CategoryService; import com.bbteam.budgetbuddies.domain.consumptiongoal.converter.ConsumptionGoalConverter; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.AllConsumptionCategoryResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.AvgConsumptionGoalDto; @@ -33,7 +37,6 @@ import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionAnalysisResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalListRequestDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalRequestDto; -import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseListDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.MonthReportResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.MyConsumptionGoalDto; @@ -42,13 +45,11 @@ import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopGoalCategoryResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.entity.ConsumptionGoal; import com.bbteam.budgetbuddies.domain.consumptiongoal.repository.ConsumptionGoalRepository; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; -import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; -import com.bbteam.budgetbuddies.domain.gemini.service.GeminiService; import com.bbteam.budgetbuddies.domain.openai.service.OpenAiService; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; +import com.bbteam.budgetbuddies.domain.user.service.UserService; import com.bbteam.budgetbuddies.enums.Gender; import lombok.RequiredArgsConstructor; @@ -62,8 +63,9 @@ public class ConsumptionGoalServiceImpl implements ConsumptionGoalService { private final ConsumptionGoalRepository consumptionGoalRepository; private final ExpenseRepository expenseRepository; private final CategoryRepository categoryRepository; + private final CategoryService categoryService; private final UserRepository userRepository; - private final GeminiService geminiService; + private final UserService userService; private final OpenAiService openAiService; private final ConsumptionGoalConverter consumptionGoalConverter; @@ -465,140 +467,48 @@ private double calculateMedian(List values) { public ConsumptionGoalResponseListDto updateConsumptionGoals(Long userId, ConsumptionGoalListRequestDto consumptionGoalListRequestDto) { LocalDate thisMonth = LocalDate.now().withDayOfMonth(1); - User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Not found user")); + User user = userService.getUser(userId); List updatedConsumptionGoal = consumptionGoalListRequestDto.getConsumptionGoalList() .stream() .map(c -> updateConsumptionGoalWithRequestDto(user, c, thisMonth)) .toList(); - List response = consumptionGoalRepository.saveAll(updatedConsumptionGoal) - .stream() - .map(consumptionGoalConverter::toConsumptionGoalResponseDto) - .toList(); - - return consumptionGoalConverter.toConsumptionGoalResponseListDto(response, thisMonth); + return consumptionGoalConverter.toConsumptionGoalResponseListDto(updatedConsumptionGoal, thisMonth); } private ConsumptionGoal updateConsumptionGoalWithRequestDto(User user, ConsumptionGoalRequestDto consumptionGoalRequestDto, LocalDate goalMonth) { - Category category = categoryRepository.findById(consumptionGoalRequestDto.getCategoryId()) - .orElseThrow(() -> new IllegalArgumentException("Not found Category")); + Category category = categoryService.getCategory(consumptionGoalRequestDto.getCategoryId()); - ConsumptionGoal consumptionGoal = findOrElseGenerateConsumptionGoal(user, category, goalMonth); + ConsumptionGoal consumptionGoal = this.getUserConsumptionGoal(user, category, goalMonth); consumptionGoal.updateGoalAmount(consumptionGoalRequestDto.getGoalAmount()); return consumptionGoal; } - private ConsumptionGoal findOrElseGenerateConsumptionGoal(User user, Category category, LocalDate goalMonth) { - return consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, category, goalMonth) - .orElseGet(() -> generateNewConsumptionGoal(user, category, goalMonth)); - } - - private ConsumptionGoal generateNewConsumptionGoal(User user, Category category, LocalDate goalMonth) { - return ConsumptionGoal.builder() - .goalMonth(goalMonth) - .user(user) - .category(category) - .consumeAmount(0L) - .goalAmount(0L) - .build(); - } - @Override - @Transactional(readOnly = true) + @Transactional public ConsumptionGoalResponseListDto findUserConsumptionGoalList(Long userId, LocalDate date) { LocalDate goalMonth = date.withDayOfMonth(1); - Map goalMap = initializeGoalMap(userId); - - updateGoalMapWithPrevious(userId, goalMonth, goalMap); - updateGoalMapWithCurrentMonth(userId, goalMonth, goalMap); - - List consumptionGoalList = new ArrayList<>(goalMap.values()); - - return consumptionGoalConverter.toConsumptionGoalResponseListDto( - orderByRemainingBalanceDescending(consumptionGoalList), goalMonth); - } + User user = userService.getUser(userId); + List categoryList = categoryService.getUserCategoryList(userId); - private Map initializeGoalMap(Long userId) { - return categoryRepository.findUserCategoryByUserId(userId) + List thisMonthUserConsumptionGoal = categoryList .stream() - .collect(Collectors.toMap(Category::getId, consumptionGoalConverter::toConsumptionGoalResponseDto)); - } - - private void updateGoalMapWithPrevious(Long userId, LocalDate goalMonth, - Map goalMap) { - goalMap.keySet() - .stream() - .map(categoryId -> consumptionGoalRepository.findLatelyGoal(userId, categoryId, goalMonth.minusMonths(1))) - .filter(Optional::isPresent) - .map(Optional::get) - .map(consumptionGoalConverter::toConsumptionGoalResponseDtoFromPreviousGoal) - .forEach(goal -> goalMap.put(goal.getCategoryId(), goal)); - } - - private void updateGoalMapWithCurrentMonth(Long userId, LocalDate goalMonth, - Map goalMap) { - consumptionGoalRepository.findConsumptionGoalByUserIdAndGoalMonth(userId, goalMonth) - .stream() - .map(consumptionGoalConverter::toConsumptionGoalResponseDto) - .forEach(goal -> goalMap.put(goal.getCategoryId(), goal)); - } - - private List orderByRemainingBalanceDescending( - List consumptionGoalList) { - return consumptionGoalList.stream() - .sorted(Comparator.comparingLong(ConsumptionGoalResponseDto::getRemainingBalance).reversed()) + .map(category -> this.getUserConsumptionGoal(user, category, goalMonth)) .toList(); + + return consumptionGoalConverter.toConsumptionGoalResponseListDto(thisMonthUserConsumptionGoal, goalMonth); } @Override @Transactional - public void recalculateConsumptionAmount(Expense expense, ExpenseUpdateRequestDto request, User user) { - restorePreviousGoalConsumptionAmount(expense, user); - calculatePresentGoalConsumptionAmount(request, user); - } - - private void restorePreviousGoalConsumptionAmount(Expense expense, User user) { - ConsumptionGoal previousConsumptionGoal = consumptionGoalRepository.findLatelyGoal(user.getId(), - expense.getCategory().getId(), expense.getExpenseDate().toLocalDate().withDayOfMonth(1)) - .orElseThrow(() -> new IllegalArgumentException("Not found consumptionGoal")); - - previousConsumptionGoal.restoreConsumeAmount(expense.getAmount()); - consumptionGoalRepository.save(previousConsumptionGoal); - } - - private void calculatePresentGoalConsumptionAmount(ExpenseUpdateRequestDto request, User user) { - Category categoryToReplace = categoryRepository.findById(request.getCategoryId()) - .orElseThrow(() -> new IllegalArgumentException("Not found category")); - - ConsumptionGoal consumptionGoal = consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth( - user, categoryToReplace, request.getExpenseDate().toLocalDate().withDayOfMonth(1)) - .orElseGet(() -> this.generateGoalByPreviousOrElseNew(user, categoryToReplace, - request.getExpenseDate().toLocalDate().withDayOfMonth(1))); - - consumptionGoal.updateConsumeAmount(request.getAmount()); - consumptionGoalRepository.save(consumptionGoal); - } - - private ConsumptionGoal generateGoalByPreviousOrElseNew(User user, Category category, LocalDate goalMonth) { - LocalDate previousMonth = goalMonth.minusMonths(1); - - return consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, category, previousMonth) - .map(this::generateGoalByPrevious) - .orElseGet(() -> generateNewConsumptionGoal(user, category, goalMonth)); - } - - private ConsumptionGoal generateGoalByPrevious(ConsumptionGoal consumptionGoal) { - return ConsumptionGoal.builder() - .goalMonth(consumptionGoal.getGoalMonth().plusMonths(1)) - .user(consumptionGoal.getUser()) - .category(consumptionGoal.getCategory()) - .consumeAmount(0L) - .goalAmount(consumptionGoal.getGoalAmount()) - .build(); + public void recalculateConsumptionAmount(ConsumptionGoal beforeConsumptionGoal, Long beforeAmount, + ConsumptionGoal afterConsumptionGoal, Long afterAmount) { + beforeConsumptionGoal.restoreConsumeAmount(beforeAmount); + afterConsumptionGoal.addConsumeAmount(afterAmount); } @Override @@ -609,10 +519,10 @@ public void updateConsumeAmount(Long userId, Long categoryId, Long amount) { .orElseThrow(() -> new IllegalArgumentException("Not found Category")); LocalDate thisMonth = LocalDate.now().withDayOfMonth(1); - ConsumptionGoal consumptionGoal = consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth( + ConsumptionGoal consumptionGoal = consumptionGoalRepository.findByUserAndCategoryAndGoalMonth( user, category, thisMonth).orElseGet(() -> generateNewConsumptionGoal(user, category, thisMonth)); - consumptionGoal.updateConsumeAmount(amount); + consumptionGoal.addConsumeAmount(amount); consumptionGoalRepository.save(consumptionGoal); } @@ -624,7 +534,7 @@ public void decreaseConsumeAmount(Long userId, Long categoryId, Long amount, Loc .orElseThrow(() -> new IllegalArgumentException("Not found Category")); LocalDate goalMonth = expenseDate.withDayOfMonth(1); - ConsumptionGoal consumptionGoal = consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth( + ConsumptionGoal consumptionGoal = consumptionGoalRepository.findByUserAndCategoryAndGoalMonth( user, category, goalMonth).orElseThrow(() -> new IllegalArgumentException("Not found ConsumptionGoal")); consumptionGoal.decreaseConsumeAmount(amount); @@ -640,12 +550,12 @@ public void updateOrCreateDeletedConsumptionGoal(Long userId, Long categoryId, L .orElseThrow(() -> new IllegalArgumentException("Invalid category ID")); // 해당 월의 ConsumptionGoal이 존재하는지 확인 - Optional existingGoal = consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth( + Optional existingGoal = consumptionGoalRepository.findByUserAndCategoryAndGoalMonth( user, category, goalMonth); if (existingGoal.isPresent()) { // 존재하는 경우, consumeAmount 업데이트 ConsumptionGoal consumptionGoal = existingGoal.get(); - consumptionGoal.updateConsumeAmount(amount); + consumptionGoal.addConsumeAmount(amount); consumptionGoalRepository.save(consumptionGoal); } else { // 존재하지 않는 경우, 새로운 ConsumptionGoal을 생성 (이 때 목표 금액은 0) ConsumptionGoal newGoal = ConsumptionGoal.builder() @@ -656,7 +566,7 @@ public void updateOrCreateDeletedConsumptionGoal(Long userId, Long categoryId, L .goalAmount(0L) .build(); - newGoal.updateConsumeAmount(amount); // 신규 생성된 목표에 소비 금액 추가 + newGoal.addConsumeAmount(amount); // 신규 생성된 목표에 소비 금액 추가 consumptionGoalRepository.save(newGoal); } } @@ -785,7 +695,7 @@ private String getMainComment(List list) { } } Optional minCategory = categoryRepository.findById(minCategoryId); - + if (minCategory.isEmpty()) { throw new IllegalArgumentException("해당 카테고리를 찾을 수 없습니다."); } @@ -795,8 +705,6 @@ private String getMainComment(List list) { long todayAvailableConsumptionAmount = minDifference / remainDays; long weekAvailableConsumptionAmount = todayAvailableConsumptionAmount * 7; - log.info(String.valueOf(weekAvailableConsumptionAmount)); - NumberFormat nf = NumberFormat.getInstance(Locale.KOREA); // 한국 단위로 locale if (weekAvailableConsumptionAmount < 0) { @@ -811,13 +719,15 @@ private String getMainComment(List list) { } @Override + @Async @Transactional(readOnly = true) - public String getConsumptionMention(Long userId) { + @Cacheable(value = "consumptionMent", key = "#userId") + public CompletableFuture getConsumptionMention(Long userId) { /** * 가장 큰 소비를 한 카테고리의 소비 목표 데이터 정보와 가장 큰 목표로 세운 카테고리의 소비 목표 데이터를 각각 가져온다. * 위 데이터들을 가지고 프롬프트 진행 - * Gemini AI, Chat GPT + * Chat GPT */ // 유저 아이디로 또래 정보 확인 @@ -835,26 +745,26 @@ public String getConsumptionMention(Long userId) { peerAgeEnd, peerGender, currentMonth); - if (!maxConsumeAmount.isPresent()) { + if (maxConsumeAmount.isEmpty()) { throw new IllegalArgumentException("해당 소비목표 데이터를 찾을 수 없습니다."); } // 유저 이름과 소비 목표 데이터로 카테고리 이름, 소비 금액을 가져 온다. String username = findUserById(userId).getName(); String categoryName = maxConsumeAmount.get().getCategory().getName(); - String consumeAmount = String.valueOf(maxConsumeAmount.get().getConsumeAmount()); + long consumeAmount = maxConsumeAmount.get().getConsumeAmount(); // 또래의 상위 소비 금액에 대한 정보로 프롬프트 작성 String firstPrompt = "00은 " + username + ", 가장 큰 소비 카테고리 이름은 " + categoryName + "," + "해당 카테고리 소비금액은" + consumeAmount + "이야"; - if (!maxGoalAmount.isPresent()) { + if (maxGoalAmount.isEmpty()) { throw new IllegalArgumentException("해당 소비목표 데이터를 찾을 수 없습니다."); } // 가장 큰 목표 소비 금액에 대한 정보로 프롬프트 작성 categoryName = maxGoalAmount.get().getCategory().getName(); - String goalAmount = String.valueOf(maxGoalAmount.get().getGoalAmount()); + long goalAmount = maxGoalAmount.get().getGoalAmount(); // 또래의 상위 목표 소비 금액에 대한 정보로 프롬프트 작성 String secondPrompt = "가장 큰 목표 소비 카테고리 이름은 " + categoryName @@ -868,8 +778,53 @@ public String getConsumptionMention(Long userId) { + "카테고리 목표 금액(ex. 패션에 N만원 소비를 계획해요)같은 트렌드 한 멘트, 인터넷상 바이럴 문구" + "참고하여 만들어줘"; - return openAiService.chat(basePrompt); - // return geminiService.getContents(basePrompt); + String response = openAiService.chat(basePrompt); + + // GPT 프롬프트 실패 시 기본 멘트 생성 반환 + if (response == null) { + NumberFormat nf = NumberFormat.getInstance(Locale.KOREA); + response = "총 " + nf.format(goalAmount - consumeAmount) + "원 더 쓸 수 있어요."; + return CompletableFuture.completedFuture(response); + } + + return CompletableFuture.completedFuture(response); + } + + @Override + @Transactional + public ConsumptionGoal getUserConsumptionGoal(User user, Category category, LocalDate goalDate) { + LocalDate goalMonth = goalDate.withDayOfMonth(1); + + return consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, goalMonth) + .orElseGet(() -> this.generateGoalFromPreviousOrNew(user, category, goalMonth)); + } + + private ConsumptionGoal generateGoalFromPreviousOrNew(User user, Category category, LocalDate goalMonth) { + LocalDate previousMonth = goalMonth.minusMonths(1); + + return consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, previousMonth) + .map(this::generateGoalByPrevious) + .orElseGet(() -> generateNewConsumptionGoal(user, category, goalMonth)); + } + + private ConsumptionGoal generateGoalByPrevious(ConsumptionGoal consumptionGoal) { + return consumptionGoalRepository.save(ConsumptionGoal.builder() + .goalMonth(consumptionGoal.getGoalMonth().plusMonths(1)) + .user(consumptionGoal.getUser()) + .category(consumptionGoal.getCategory()) + .consumeAmount(0L) + .goalAmount(consumptionGoal.getGoalAmount()) + .build()); + } + + private ConsumptionGoal generateNewConsumptionGoal(User user, Category category, LocalDate goalMonth) { + return consumptionGoalRepository.save(ConsumptionGoal.builder() + .goalMonth(goalMonth) + .user(user) + .category(category) + .consumeAmount(0L) + .goalAmount(0L) + .build()); } } \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoApi.java b/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoApi.java index a3c880a2..f2e0bf76 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoApi.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoApi.java @@ -1,12 +1,17 @@ package com.bbteam.budgetbuddies.domain.discountinfo.controller; import com.bbteam.budgetbuddies.apiPayload.ApiResponse; +import com.bbteam.budgetbuddies.apiPayload.code.ErrorReasonDto; import com.bbteam.budgetbuddies.domain.discountinfo.dto.DiscountRequest; import com.bbteam.budgetbuddies.domain.discountinfo.dto.DiscountResponseDto; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; import com.bbteam.budgetbuddies.domain.user.validation.ExistUser; +import com.bbteam.budgetbuddies.global.security.utils.AuthUser; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.PathVariable; @@ -17,10 +22,14 @@ public interface DiscountInfoApi { @Operation(summary = "[User] 특정 년월 할인정보 리스트 가져오기 API", description = "특정 년도와 월에 해당하는 할인정보 목록을 조회하는 API이며, 페이징을 포함합니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "year", description = "데이터를 가져올 연도입니다."), @@ -37,11 +46,13 @@ ApiResponse> getDiscountsByYearAndMonth( @Operation(summary = "[ADMIN] 할인정보 등록하기 API", description = "할인정보를 등록하는 API이며, 추후에는 관리자만 접근 가능하도록 할 예정입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) - }) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ // @Parameter(name = "discountRequestDto", description = "등록할 할인 정보의 전체 내용입니다."), }) @@ -51,27 +62,31 @@ ApiResponse registerDiscountInfo( @Operation(summary = "[User] 특정 할인정보에 좋아요 클릭 API", description = "특정 할인정보에 좋아요 버튼을 클릭하는 API이며, 일단은 사용자 ID를 입력하여 사용합니다. (추후 토큰으로 검증)") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) - }) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ - @Parameter(name = "userId", description = "좋아요를 누른 사용자의 id입니다."), @Parameter(name = "discountInfoId", description = "좋아요를 누를 할인정보의 id입니다."), }) ApiResponse likeDiscountInfo( - @RequestParam @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @PathVariable Long discountInfoId ); @Operation(summary = "[ADMIN] 특정 할인정보 수정하기 API", description = "특정 할인정보를 수정하는 API입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) - }) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ }) ApiResponse updateDiscountInfo( @@ -80,10 +95,13 @@ ApiResponse updateDiscountInfo( @Operation(summary = "[ADMIN] 특정 할인정보 삭제하기 API", description = "ID를 통해 특정 할인정보를 삭제하는 API입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "discountInfoId", description = "삭제할 할인 정보의 id입니다."), @@ -94,11 +112,14 @@ ApiResponse deleteDiscountInfo( @Operation(summary = "[ADMIN] 특정 할인정보 가져오기 API", description = "ID를 통해 특정 할인정보를 가져오는 API입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) - }) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "discountInfoId", description = "조회할 할인 정보의 id입니다."), }) @@ -108,18 +129,21 @@ ApiResponse getDiscountInfo( @Operation(summary = "[ADMIN] 특정 사용자가 좋아요를 누른 할인정보 가져오기 API", description = "특정 사용자가 좋아요를 누른 할인정보들을 가져오는 API입니다. 페이징을 포함합니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) - }) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "userId", description = "특정 사용자의 id입니다."), @Parameter(name = "page", description = "페이지 번호, 0번이 1 페이지 입니다. (기본값은 0입니다.)"), @Parameter(name = "size", description = "한 페이지에 불러올 데이터 개수입니다. (기본값은 10개입니다.)") }) ApiResponse> getLikedDiscountInfo( - @PathVariable @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size ); diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoController.java b/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoController.java index 2adb897c..9a29fe89 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoController.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/discountinfo/controller/DiscountInfoController.java @@ -4,7 +4,8 @@ import com.bbteam.budgetbuddies.domain.discountinfo.dto.DiscountRequest; import com.bbteam.budgetbuddies.domain.discountinfo.dto.DiscountResponseDto; import com.bbteam.budgetbuddies.domain.discountinfo.service.DiscountInfoService; -import com.bbteam.budgetbuddies.domain.user.validation.ExistUser; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; +import com.bbteam.budgetbuddies.global.security.utils.AuthUser; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; @@ -42,10 +43,10 @@ public ApiResponse registerDiscountInfo( @Override @PostMapping("/likes/{discountInfoId}") public ApiResponse likeDiscountInfo( - @RequestParam @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @PathVariable Long discountInfoId ) { - DiscountResponseDto discountResponseDto = discountInfoService.toggleLike(userId, discountInfoId); + DiscountResponseDto discountResponseDto = discountInfoService.toggleLike(user.getId(), discountInfoId); return ApiResponse.onSuccess(discountResponseDto); } @@ -84,11 +85,11 @@ public ApiResponse getDiscountInfo( @Override @GetMapping("/liked-all") public ApiResponse> getLikedDiscountInfo( - @RequestParam @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size ) { - Page likedDiscountInfoPage = discountInfoService.getLikedDiscountInfo(userId, page, size); + Page likedDiscountInfoPage = discountInfoService.getLikedDiscountInfo(user.getId(), page, size); return ApiResponse.onSuccess(likedDiscountInfoPage); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java index 50a8cc9a..45856956 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java @@ -2,20 +2,16 @@ import java.time.LocalDate; -import org.springframework.data.repository.query.Param; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; +import com.bbteam.budgetbuddies.domain.expense.dto.DetailExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -31,9 +27,9 @@ public interface ExpenseApi { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class)))}) - ResponseEntity createExpense( - @Parameter(description = "user_id") @PathVariable Long userId, - @Parameter(description = "category_id, amount, description, expenseDate") @RequestBody ExpenseRequestDto expenseRequestDto + ResponseEntity createExpense( + @Parameter(description = "user_id") @PathVariable Long userId, + @Parameter(description = "category_id, amount, description, expenseDate") @RequestBody ExpenseRequestDto expenseRequestDto ); @Operation(summary = "[User] 월별 소비 조회", description = "무한 스크롤을 통한 조회로 예상하여 Slice를 통해서 조회") @@ -43,8 +39,8 @@ ResponseEntity createExpense( @ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class)))}) ResponseEntity findExpensesForMonth( - @PathVariable @Param("userId") Long userId, - @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date); + UserDto.AuthUserDto user, + LocalDate date); @Operation(summary = "[User] 단일 소비 조회하기", description = "queryParameter를 통해 소비 Id를 전달 받아서 응답값을 조회") @ApiResponses({ @@ -52,8 +48,7 @@ ResponseEntity findExpensesForMonth( @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class)))}) - @GetMapping("/{userId}/{expenseId}") - ResponseEntity findExpense(@Param("userId") Long userId, @Param("expenseId") Long expenseId); + ResponseEntity findExpense(UserDto.AuthUserDto user, Long expenseId); @Operation(summary = "[User] 단일 소비 업데이트하기", description = "소비 아이디와 카테고리 아이디, amount(소비 금액)을 body에 담아서 소비를 업데이트") @ApiResponses({ @@ -61,10 +56,7 @@ ResponseEntity findExpensesForMonth( @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), @ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class)))}) - @GetMapping("/{userId}/{expenseId}") - @PostMapping("/{userId}") - ResponseEntity updateExpense(@PathVariable @Param("userId") Long userId, - @RequestBody ExpenseUpdateRequestDto request); + ResponseEntity updateExpense(UserDto.AuthUserDto user, ExpenseUpdateRequestDto request); @Operation(summary = "[User] 소비 내역 삭제", description = "사용자가 소비 내역을 삭제합니다.") @ApiResponses({ diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java index c6499330..75329f71 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java @@ -15,10 +15,12 @@ import org.springframework.web.bind.annotation.RestController; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.DetailExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.service.ExpenseService; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; +import com.bbteam.budgetbuddies.global.security.utils.AuthUser; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; @@ -31,41 +33,37 @@ public class ExpenseController implements ExpenseApi { @Override @PostMapping("/add/{userId}") - public ResponseEntity createExpense( - @Parameter(description = "user_id") @PathVariable Long userId, - @Parameter(description = "category_id, amount, description, expenseDate") @RequestBody ExpenseRequestDto expenseRequestDto) { - ExpenseResponseDto response = expenseService.createExpense(userId, expenseRequestDto); + public ResponseEntity createExpense( + @Parameter(description = "user_id") @PathVariable Long userId, + @Parameter(description = "category_id, amount, description, expenseDate") @RequestBody ExpenseRequestDto expenseRequestDto) { + DetailExpenseResponseDto response = expenseService.createExpense(userId, expenseRequestDto); return ResponseEntity.ok(response); } @Override - @GetMapping("/{userId}") - public ResponseEntity findExpensesForMonth( - @PathVariable @Param("userId") Long userId, + @GetMapping() + public ResponseEntity findExpensesForMonth(@AuthUser UserDto.AuthUserDto user, @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) { - return ResponseEntity.ok(expenseService.getMonthlyExpense(userId, date)); + return ResponseEntity.ok(expenseService.getMonthlyExpense(user.getId(), date)); } @Override - @GetMapping("/{userId}/{expenseId}") - public ResponseEntity findExpense(@PathVariable @Param("userId") Long userId, + @GetMapping("/{expenseId}") + public ResponseEntity findExpense(@AuthUser UserDto.AuthUserDto user, @PathVariable @Param("expenseId") Long expenseId) { - return ResponseEntity.ok(expenseService.findExpenseResponseFromUserIdAndExpenseId(userId, expenseId)); + return ResponseEntity.ok(expenseService.findDetailExpenseResponse(user.getId(), expenseId)); } @Override - @PostMapping("/{userId}") - public ResponseEntity updateExpense(@PathVariable @Param("userId") Long userId, + @PostMapping() + public ResponseEntity updateExpense(@AuthUser UserDto.AuthUserDto user, @RequestBody ExpenseUpdateRequestDto request) { - ExpenseResponseDto response = expenseService.updateExpense(userId, request); - return ResponseEntity.ok(response); + return ResponseEntity.ok(expenseService.updateExpense(user.getId(), request)); } @DeleteMapping("/delete/{expenseId}") - public ResponseEntity deleteExpense( - @Parameter(description = "expense_id") - @PathVariable Long expenseId) { + public ResponseEntity deleteExpense(@Parameter(description = "expense_id") @PathVariable Long expenseId) { expenseService.deleteExpense(expenseId); return ResponseEntity.ok("Successfully deleted expense!"); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java index 665849d1..3c639164 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java @@ -13,7 +13,7 @@ import com.bbteam.budgetbuddies.domain.expense.dto.CompactExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.DailyExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.DetailExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.user.entity.User; @@ -31,10 +31,9 @@ public Expense toExpenseEntity(ExpenseRequestDto expenseRequestDto, User user, C .build(); } - public ExpenseResponseDto toExpenseResponseDto(Expense expense) { - return ExpenseResponseDto.builder() + public DetailExpenseResponseDto toDetailExpenseResponseDto(Expense expense) { + return DetailExpenseResponseDto.builder() .expenseId(expense.getId()) - .userId(expense.getUser().getId()) .categoryId(expense.getCategory().getId()) .categoryName(expense.getCategory().getName()) .amount(expense.getAmount()) diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseResponseDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/DetailExpenseResponseDto.java similarity index 91% rename from src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseResponseDto.java rename to src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/DetailExpenseResponseDto.java index f0996be0..ef0fdef5 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseResponseDto.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/DetailExpenseResponseDto.java @@ -13,9 +13,8 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class ExpenseResponseDto { +public class DetailExpenseResponseDto { private Long expenseId; - private Long userId; private Long categoryId; private String categoryName; private Long amount; diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseUpdateRequestDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseUpdateRequestDto.java index 14312ca2..464cd098 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseUpdateRequestDto.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/ExpenseUpdateRequestDto.java @@ -16,9 +16,11 @@ @Builder public class ExpenseUpdateRequestDto { private Long expenseId; + private Long categoryId; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime expenseDate; + private Long amount; } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java index b55c55fd..62279b8b 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java @@ -14,8 +14,8 @@ import com.bbteam.budgetbuddies.enums.Gender; public interface ExpenseRepository extends JpaRepository { - @Query("SELECT e FROM Expense e WHERE e.user = :user AND e.expenseDate BETWEEN :startDate AND :endDate ORDER BY e.expenseDate DESC") - List findAllByUserIdForPeriod(@Param("user") User user, @Param("startDate") LocalDateTime startDate, + @Query("SELECT e FROM Expense e WHERE e.user.id = :userId AND e.expenseDate >= :startDate AND e.expenseDate < :endDate ORDER BY e.expenseDate DESC") + List findAllByUserIdForPeriod(@Param("userId") Long userId, @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); List findByCategoryIdAndUserIdAndExpenseDateBetweenAndDeletedFalse(Long categoryId, Long userId, diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java index 67df0f7e..b14c8332 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java @@ -3,18 +3,18 @@ import java.time.LocalDate; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.DetailExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseResponseDto; public interface ExpenseService { - ExpenseResponseDto createExpense(Long userId, ExpenseRequestDto expenseRequestDto); + DetailExpenseResponseDto createExpense(Long userId, ExpenseRequestDto expenseRequestDto); MonthlyExpenseResponseDto getMonthlyExpense(Long userId, LocalDate localDate); - ExpenseResponseDto findExpenseResponseFromUserIdAndExpenseId(Long userId, Long expenseId); + DetailExpenseResponseDto findDetailExpenseResponse(Long userId, Long expenseId); - ExpenseResponseDto updateExpense(Long userId, ExpenseUpdateRequestDto request); + DetailExpenseResponseDto updateExpense(Long userId, ExpenseUpdateRequestDto request); void deleteExpense(Long expenseId); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java index a51df924..5d6bc445 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java @@ -1,7 +1,6 @@ package com.bbteam.budgetbuddies.domain.expense.service; import java.time.LocalDate; -import java.time.LocalTime; import java.util.List; import org.springframework.stereotype.Service; @@ -10,16 +9,18 @@ import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; import com.bbteam.budgetbuddies.domain.category.service.CategoryService; +import com.bbteam.budgetbuddies.domain.consumptiongoal.entity.ConsumptionGoal; import com.bbteam.budgetbuddies.domain.consumptiongoal.service.ConsumptionGoalService; import com.bbteam.budgetbuddies.domain.expense.converter.ExpenseConverter; +import com.bbteam.budgetbuddies.domain.expense.dto.DetailExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; +import com.bbteam.budgetbuddies.domain.user.service.UserService; import lombok.RequiredArgsConstructor; @@ -29,13 +30,14 @@ public class ExpenseServiceImpl implements ExpenseService { private final ExpenseRepository expenseRepository; private final UserRepository userRepository; + private final UserService userService; private final CategoryRepository categoryRepository; private final CategoryService categoryService; private final ExpenseConverter expenseConverter; private final ConsumptionGoalService consumptionGoalService; @Override - public ExpenseResponseDto createExpense(Long userId, ExpenseRequestDto expenseRequestDto) { + public DetailExpenseResponseDto createExpense(Long userId, ExpenseRequestDto expenseRequestDto) { User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); Category category = categoryRepository.findById(expenseRequestDto.getCategoryId()) @@ -73,14 +75,15 @@ else if (!category.getIsDefault() && category.getUser().getId().equals(userId)) if (expenseDateMonth.equals(currentMonth)) { // 현재 월의 소비 내역일 경우 ConsumptionGoal을 업데이트 - consumptionGoalService.updateConsumeAmount(userId, expenseRequestDto.getCategoryId(), expenseRequestDto.getAmount()); + consumptionGoalService.updateConsumeAmount(userId, expenseRequestDto.getCategoryId(), + expenseRequestDto.getAmount()); } -// else { -// // 과거 월의 소비 내역일 경우 해당 월의 ConsumptionGoal을 업데이트 또는 삭제 상태로 생성 -// consumptionGoalService.updateOrCreateDeletedConsumptionGoal(userId, expenseRequestDto.getCategoryId(), expenseDateMonth, expenseRequestDto.getAmount()); -// } + // else { + // // 과거 월의 소비 내역일 경우 해당 월의 ConsumptionGoal을 업데이트 또는 삭제 상태로 생성 + // consumptionGoalService.updateOrCreateDeletedConsumptionGoal(userId, expenseRequestDto.getCategoryId(), expenseDateMonth, expenseRequestDto.getAmount()); + // } - return expenseConverter.toExpenseResponseDto(expense); + return expenseConverter.toDetailExpenseResponseDto(expense); /* Case 1 결과) 해당 유저의 user_id + immutable 필드 중 하나의 조합으로 Expense 테이블에 저장 Case 2 결과) 내가 직접 생성한 카테고리 중 하나로 카테고리를 설정하여 Expense 테이블에 저장 @@ -91,7 +94,7 @@ else if (!category.getIsDefault() && category.getUser().getId().equals(userId)) @Transactional public void deleteExpense(Long expenseId) { Expense expense = expenseRepository.findById(expenseId) - .orElseThrow(() -> new IllegalArgumentException("Not found Expense")); + .orElseThrow(() -> new IllegalArgumentException("Not found Expense")); Long userId = expense.getUser().getId(); Long categoryId = expense.getCategory().getId(); @@ -113,45 +116,42 @@ public void deleteExpense(Long expenseId) { @Transactional(readOnly = true) public MonthlyExpenseResponseDto getMonthlyExpense(Long userId, LocalDate localDate) { LocalDate startOfMonth = localDate.withDayOfMonth(1); - LocalDate endOfMonth = localDate.withDayOfMonth(startOfMonth.lengthOfMonth()); + LocalDate nextMonth = startOfMonth.plusMonths(1L); - User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); - - List expenseSlice = expenseRepository.findAllByUserIdForPeriod(user, - startOfMonth.atStartOfDay(), endOfMonth.atTime(LocalTime.MAX)); + List expenseSlice = expenseRepository.findAllByUserIdForPeriod(userId, + startOfMonth.atStartOfDay(), nextMonth.atStartOfDay()); return expenseConverter.toMonthlyExpenseResponseDto(expenseSlice, startOfMonth); } @Override - public ExpenseResponseDto findExpenseResponseFromUserIdAndExpenseId(Long userId, Long expenseId) { - Expense expense = expenseRepository.findById(expenseId) - .orElseThrow(() -> new IllegalArgumentException("Not found expense")); - - checkUserAuthority(userId, expense); - - return expenseConverter.toExpenseResponseDto(expense); + public DetailExpenseResponseDto findDetailExpenseResponse(Long userId, Long expenseId) { + return expenseConverter.toDetailExpenseResponseDto(getExpense(expenseId)); } - private void checkUserAuthority(Long userId, Expense expense) { - if (!expense.getUser().getId().equals(userId)) - throw new IllegalArgumentException("Unauthorized user"); + private Expense getExpense(Long expenseId) { + return expenseRepository.findById(expenseId) + .orElseThrow(() -> new IllegalArgumentException("Not found expense")); } @Override @Transactional - public ExpenseResponseDto updateExpense(Long userId, ExpenseUpdateRequestDto request) { - Expense expense = expenseRepository.findById(request.getExpenseId()) - .orElseThrow(() -> new IllegalArgumentException("Not found expense")); + public DetailExpenseResponseDto updateExpense(Long userId, ExpenseUpdateRequestDto request) { + User user = userService.getUser(userId); + Expense expense = getExpense(request.getExpenseId()); + Category categoryToReplace = categoryService.getCategory(request.getCategoryId()); - User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Not found user")); - checkUserAuthority(userId, expense); + expense.updateExpenseFromRequest(request, categoryToReplace); - Category categoryToReplace = categoryService.handleCategoryChange(expense, request, user); + ConsumptionGoal beforeConsumptionGoal = consumptionGoalService.getUserConsumptionGoal( + user, expense.getCategory(), expense.getExpenseDate().toLocalDate().withDayOfMonth(1)); + ConsumptionGoal afterConsumptionGoal = consumptionGoalService.getUserConsumptionGoal( + user, categoryToReplace, request.getExpenseDate().toLocalDate().withDayOfMonth(1)); - expense.updateExpenseFromRequest(request, categoryToReplace); + consumptionGoalService.recalculateConsumptionAmount(beforeConsumptionGoal, expense.getAmount(), + afterConsumptionGoal, request.getAmount()); - return expenseConverter.toExpenseResponseDto(expenseRepository.save(expense)); + return expenseConverter.toDetailExpenseResponseDto(expenseRepository.save(expense)); } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqApi.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqApi.java index f3e0b134..8485960f 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqApi.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqApi.java @@ -17,6 +17,7 @@ import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @@ -70,7 +71,7 @@ ApiResponse postFaq(@ExistUser @RequestParam Long userId, } ) - ApiResponse findByPaging(Pageable pageable); + ApiResponse findByPaging(Pageable pageable, String SearchCondition); @Operation(summary = "[User] FAQ 수정 API", description = "FAQ를 수정하는 API입니다.", requestBody = @RequestBody( @@ -112,4 +113,9 @@ ApiResponse modifyFaq(@PathVariable @ExistFaq Long faqId, } ) ApiResponse deleteFaq(@ExistFaq Long faqId); + + ApiResponse addKeyword(@ExistFaq Long faqId, Long searchKeywordId); + + ApiResponse deleteKeyword(@ExistFaq Long faqId, Long searchKeywordId); + } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqController.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqController.java index c8da6681..c15fe5d2 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqController.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/controller/FaqController.java @@ -7,10 +7,12 @@ import com.bbteam.budgetbuddies.domain.faq.validation.ExistFaq; import com.bbteam.budgetbuddies.domain.user.validation.ExistUser; import jakarta.validation.Valid; +import jakarta.validation.constraints.Null; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.lang.Nullable; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -37,8 +39,9 @@ public ApiResponse findFaq(@PathVariable @ExistF @Override @GetMapping("/all") - public ApiResponse> findByPaging(@ParameterObject Pageable pageable) { - return ApiResponse.onSuccess(faqService.findAllWithPaging(pageable)); + public ApiResponse> findByPaging(@ParameterObject Pageable pageable, + @RequestParam @Nullable String searchCondition) { + return ApiResponse.onSuccess(faqService.searchFaq(pageable, searchCondition)); } @Override @@ -54,4 +57,16 @@ public ApiResponse deleteFaq(@PathVariable @ExistFaq Long faqId) { faqService.deleteFaq(faqId); return ApiResponse.onSuccess("Delete Success"); } + + @Override + @PostMapping("/{faqId}/keyword") + public ApiResponse addKeyword(@PathVariable @ExistFaq Long faqId, @RequestParam Long searchKeywordId) { + return ApiResponse.onSuccess(faqService.addKeyword(faqId, searchKeywordId)); + } + + @Override + @DeleteMapping("/{faqId}/keyword") + public ApiResponse deleteKeyword(@PathVariable @ExistFaq Long faqId, @RequestParam Long searchKeywordId) { + return ApiResponse.onSuccess(faqService.removeKeyword(faqId, searchKeywordId)); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepository.java index 4ab1c63e..d9c6cf4f 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepository.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepository.java @@ -3,7 +3,7 @@ import com.bbteam.budgetbuddies.domain.faq.entity.Faq; import org.springframework.data.jpa.repository.JpaRepository; -public interface FaqRepository extends JpaRepository { +public interface FaqRepository extends JpaRepository, FaqSearchRepository { } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqSearchRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqSearchRepository.java new file mode 100644 index 00000000..b54ab5be --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqSearchRepository.java @@ -0,0 +1,10 @@ +package com.bbteam.budgetbuddies.domain.faq.repository; + +import com.bbteam.budgetbuddies.domain.faq.entity.Faq; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FaqSearchRepository { + + Page searchFaq(Pageable pageable, String searchCondition); +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqSearchRepositoryImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqSearchRepositoryImpl.java new file mode 100644 index 00000000..1e3116ab --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqSearchRepositoryImpl.java @@ -0,0 +1,61 @@ +package com.bbteam.budgetbuddies.domain.faq.repository; + +import com.bbteam.budgetbuddies.domain.faq.entity.Faq; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.QSearchKeyword; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static com.bbteam.budgetbuddies.domain.faq.entity.QFaq.*; +import static com.bbteam.budgetbuddies.domain.faqkeyword.domain.QFaqKeyword.*; +import static com.bbteam.budgetbuddies.domain.searchkeyword.domain.QSearchKeyword.*; + +public class FaqSearchRepositoryImpl implements FaqSearchRepository{ + + private final JPAQueryFactory queryFactory; + + public FaqSearchRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page searchFaq(Pageable pageable, String searchCondition) { + List result = queryFactory.select(faq) + .from(faq) + .where(faq.id.in( + JPAExpressions + .select(faqKeyword.faq.id) + .from(faqKeyword) + .join(searchKeyword).on(keywordMatch(searchCondition)) + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory.select(faq.count()) + .from(faq) + .where(faq.id.in( + JPAExpressions + .select(faqKeyword.faq.id) + .from(faqKeyword) + .join(searchKeyword).on(keywordMatch(searchCondition)) + )) + .fetchOne(); + + return new PageImpl<>(result, pageable, total); + + } + + private BooleanExpression keywordMatch(String searchCondition) { + return searchCondition != null ? searchKeyword.keyword.contains(searchCondition) : null; + } + +} + diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqService.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqService.java index eb6a3718..f334dac5 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqService.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqService.java @@ -2,6 +2,7 @@ import com.bbteam.budgetbuddies.domain.faq.dto.FaqRequestDto; import com.bbteam.budgetbuddies.domain.faq.dto.FaqResponseDto; +import com.bbteam.budgetbuddies.domain.faqkeyword.dto.FaqKeywordResponseDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -11,9 +12,15 @@ public interface FaqService { Page findAllWithPaging(Pageable pageable); + Page searchFaq(Pageable pageable, String searchCondition); + FaqResponseDto.FaqPostResponse postFaq(FaqRequestDto.FaqPostRequest dto, Long userId); FaqResponseDto.FaqModifyResponse modifyFaq(FaqRequestDto.FaqModifyRequest dto, Long faqId); String deleteFaq(Long faqId); + + FaqKeywordResponseDto addKeyword(Long faqId, Long searchKeywordId); + + String removeKeyword(Long faqId, Long searchKeywordId); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceImpl.java index a28ccba8..6b732095 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceImpl.java @@ -7,6 +7,11 @@ import com.bbteam.budgetbuddies.domain.faq.dto.FaqResponseDto; import com.bbteam.budgetbuddies.domain.faq.entity.Faq; import com.bbteam.budgetbuddies.domain.faq.repository.FaqRepository; +import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword; +import com.bbteam.budgetbuddies.domain.faqkeyword.dto.FaqKeywordResponseDto; +import com.bbteam.budgetbuddies.domain.faqkeyword.repository.FaqKeywordRepository; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import com.bbteam.budgetbuddies.domain.searchkeyword.repository.SearchKeywordRepository; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -22,6 +27,8 @@ public class FaqServiceImpl implements FaqService{ private final FaqRepository faqRepository; private final UserRepository userRepository; + private final FaqKeywordRepository faqKeywordRepository; + private final SearchKeywordRepository searchKeywordRepository; @Override public FaqResponseDto.FaqFindResponse findOneFaq(Long faqId) { @@ -62,4 +69,36 @@ public String deleteFaq(Long faqId) { private Faq findFaq(Long faqId) { return faqRepository.findById(faqId).orElseThrow(() -> new GeneralException(ErrorStatus._FAQ_NOT_FOUND)); } + + @Override + public Page searchFaq(Pageable pageable, String searchCondition) { + return faqRepository.searchFaq(pageable, searchCondition).map(FaqConverter::entityToFind); + } + + @Override + @Transactional + public FaqKeywordResponseDto addKeyword(Long faqId, Long searchKeywordId) { + Faq faq = findFaq(faqId); + SearchKeyword searchKeyword = searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND)); + + FaqKeyword faqKeyword = FaqKeyword.builder() + .searchKeyword(searchKeyword) + .faq(faq) + .build(); + + faqKeywordRepository.save(faqKeyword); + return FaqKeywordResponseDto.toDto(faqKeyword); + } + + @Override + @Transactional + public String removeKeyword(Long faqId, Long searchKeywordId) { + Faq faq = findFaq(faqId); + SearchKeyword searchKeyword = searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND)); + + FaqKeyword faqKeyword = faqKeywordRepository.findByFaqAndSearchKeyword(faq, searchKeyword).orElseThrow(() -> new GeneralException(ErrorStatus._FAQ_KEYWORD_NOT_FOUND)); + faqKeywordRepository.delete(faqKeyword); + + return "ok"; + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/domain/FaqKeyword.java b/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/domain/FaqKeyword.java new file mode 100644 index 00000000..6f5e9f14 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/domain/FaqKeyword.java @@ -0,0 +1,32 @@ +package com.bbteam.budgetbuddies.domain.faqkeyword.domain; + +import com.bbteam.budgetbuddies.common.BaseEntity; +import com.bbteam.budgetbuddies.domain.faq.entity.Faq; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@SuperBuilder +public class FaqKeyword extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "faq_id") + @NotFound(action = NotFoundAction.IGNORE) + private Faq faq; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "search_keyword_id") + @NotFound(action = NotFoundAction.IGNORE) + private SearchKeyword searchKeyword; + +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/dto/FaqKeywordResponseDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/dto/FaqKeywordResponseDto.java new file mode 100644 index 00000000..da6ef042 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/dto/FaqKeywordResponseDto.java @@ -0,0 +1,21 @@ +package com.bbteam.budgetbuddies.domain.faqkeyword.dto; + +import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class FaqKeywordResponseDto { + + private Long faqId; + private Long searchKeywordId; + + private String faqTitle; + private String keyword; + + public static FaqKeywordResponseDto toDto(FaqKeyword faqKeyword) { + return new FaqKeywordResponseDto(faqKeyword.getFaq().getId(), faqKeyword.getSearchKeyword().getId(), + faqKeyword.getFaq().getTitle(), faqKeyword.getSearchKeyword().getKeyword()); + } +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/repository/FaqKeywordRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/repository/FaqKeywordRepository.java new file mode 100644 index 00000000..c0606ba0 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/faqkeyword/repository/FaqKeywordRepository.java @@ -0,0 +1,12 @@ +package com.bbteam.budgetbuddies.domain.faqkeyword.repository; + +import com.bbteam.budgetbuddies.domain.faq.entity.Faq; +import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FaqKeywordRepository extends JpaRepository { + Optional findByFaqAndSearchKeyword(Faq faq, SearchKeyword searchKeyword); +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/controller/GeminiController.java b/src/main/java/com/bbteam/budgetbuddies/domain/gemini/controller/GeminiController.java deleted file mode 100644 index 1222baa2..00000000 --- a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/controller/GeminiController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bbteam.budgetbuddies.domain.gemini.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpClientErrorException; - -import com.bbteam.budgetbuddies.domain.gemini.service.GeminiServiceImpl; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/gemini") -public class GeminiController { - - private final GeminiServiceImpl geminiService; - - @GetMapping("/chat") - public ResponseEntity gemini(@RequestParam(name = "message") String message) { - try { - return ResponseEntity.ok().body(geminiService.getContents(message)); - } catch (HttpClientErrorException e) { - return ResponseEntity.badRequest().body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/dto/ChatRequest.java b/src/main/java/com/bbteam/budgetbuddies/domain/gemini/dto/ChatRequest.java deleted file mode 100644 index 31dbf2c1..00000000 --- a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/dto/ChatRequest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.bbteam.budgetbuddies.domain.gemini.dto; - -import java.util.ArrayList; -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class ChatRequest { - private List contents; - private GenerationConfig generationConfig; - - @Getter - @Setter - public static class Content { - private Parts parts; - } - - @Getter - @Setter - public static class Parts { - private String text; - - } - - @Getter - @Setter - public static class GenerationConfig { - private int candidate_count; - private int max_output_tokens; - private double temperature; - - } - - public ChatRequest(String prompt) { - this.contents = new ArrayList<>(); - Content content = new Content(); - Parts parts = new Parts(); - - parts.setText(prompt); - content.setParts(parts); - - this.contents.add(content); - this.generationConfig = new GenerationConfig(); - this.generationConfig.setCandidate_count(1); - this.generationConfig.setMax_output_tokens(1000); - this.generationConfig.setTemperature(0.7); - } -} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/dto/ChatResponse.java b/src/main/java/com/bbteam/budgetbuddies/domain/gemini/dto/ChatResponse.java deleted file mode 100644 index 3ff240cb..00000000 --- a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/dto/ChatResponse.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.bbteam.budgetbuddies.domain.gemini.dto; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ChatResponse { - - private List candidates; - private PromptFeedback promptFeedback; - - @Getter - @Setter - public static class Candidate { - private Content content; - private String finishReason; - private int index; - private List safetyRatings; - - } - - @Getter - @Setter - @ToString - public static class Content { - private List parts; - private String role; - - } - - @Getter - @Setter - @ToString - public static class Parts { - private String text; - - } - - @Getter - @Setter - public static class SafetyRating { - private String category; - private String probability; - } - - @Getter - @Setter - public static class PromptFeedback { - private List safetyRatings; - - } -} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/service/GeminiService.java b/src/main/java/com/bbteam/budgetbuddies/domain/gemini/service/GeminiService.java deleted file mode 100644 index eb1f80fb..00000000 --- a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/service/GeminiService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.bbteam.budgetbuddies.domain.gemini.service; - -public interface GeminiService { - - String getContents(String prompt); - -} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/service/GeminiServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/gemini/service/GeminiServiceImpl.java deleted file mode 100644 index a1c727a5..00000000 --- a/src/main/java/com/bbteam/budgetbuddies/domain/gemini/service/GeminiServiceImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.bbteam.budgetbuddies.domain.gemini.service; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import com.bbteam.budgetbuddies.domain.gemini.dto.ChatRequest; -import com.bbteam.budgetbuddies.domain.gemini.dto.ChatResponse; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class GeminiServiceImpl implements GeminiService { - - @Qualifier("geminiRestTemplate") - @Autowired - private RestTemplate restTemplate; - - @Value("${spring.gemini.api.url}") - private String apiUrl; - - @Value("${spring.gemini.api.key}") - private String geminiApiKey; - - public String getContents(String prompt) { - - String requestUrl = apiUrl + "?key=" + geminiApiKey; - - ChatRequest request = new ChatRequest(prompt); - ChatResponse response = restTemplate.postForObject(requestUrl, request, ChatResponse.class); - - String message = response.getCandidates().get(0).getContent().getParts().get(0).getText().toString(); - - return message; - } - -} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/controller/SearchKeywordController.java b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/controller/SearchKeywordController.java new file mode 100644 index 00000000..45786bbe --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/controller/SearchKeywordController.java @@ -0,0 +1,48 @@ +package com.bbteam.budgetbuddies.domain.searchkeyword.controller; + +import com.bbteam.budgetbuddies.apiPayload.ApiResponse; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import com.bbteam.budgetbuddies.domain.searchkeyword.service.SearchKeywordService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/search-keyword") +public class SearchKeywordController { + + private final SearchKeywordService searchKeywordService; + + @PostMapping("") + public ApiResponse saveKeyword(String keyword) { + + return ApiResponse.onSuccess(searchKeywordService.saveKeyword(keyword)); + } + + @GetMapping("") + public ApiResponse findOne(Long searchKeywordId) { + return ApiResponse.onSuccess(searchKeywordService.findOne(searchKeywordId)); + } + + @GetMapping("/all") + public ApiResponse> findAll(Pageable pageable) { + return ApiResponse.onSuccess(searchKeywordService.findAll(pageable)); + } + + @PutMapping("") + public ApiResponse modifyOne(Long searchKeywordId, String newKeyword) { + return ApiResponse.onSuccess((searchKeywordService.modifyOne(searchKeywordId, newKeyword))); + } + + @DeleteMapping("") + public ApiResponse deleteOne(Long searchKeywordId) { + searchKeywordService.deleteOne(searchKeywordId); + return ApiResponse.onSuccess("OK"); + } + + + + +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/domain/SearchKeyword.java b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/domain/SearchKeyword.java new file mode 100644 index 00000000..970d72f4 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/domain/SearchKeyword.java @@ -0,0 +1,23 @@ +package com.bbteam.budgetbuddies.domain.searchkeyword.domain; + +import com.bbteam.budgetbuddies.common.BaseEntity; +import jakarta.persistence.Entity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SearchKeyword extends BaseEntity { + + private String keyword; + + public void changeKeyword(String newKeyword) { + this.keyword = newKeyword; + } + +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/repository/SearchKeywordRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/repository/SearchKeywordRepository.java new file mode 100644 index 00000000..1fcea8b7 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/repository/SearchKeywordRepository.java @@ -0,0 +1,7 @@ +package com.bbteam.budgetbuddies.domain.searchkeyword.repository; + +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SearchKeywordRepository extends JpaRepository { +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/service/SearchKeywordService.java b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/service/SearchKeywordService.java new file mode 100644 index 00000000..b960855d --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/searchkeyword/service/SearchKeywordService.java @@ -0,0 +1,44 @@ +package com.bbteam.budgetbuddies.domain.searchkeyword.service; + +import com.bbteam.budgetbuddies.apiPayload.code.status.ErrorStatus; +import com.bbteam.budgetbuddies.apiPayload.exception.GeneralException; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import com.bbteam.budgetbuddies.domain.searchkeyword.repository.SearchKeywordRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class SearchKeywordService { + + private final SearchKeywordRepository searchKeywordRepository; + + public SearchKeyword saveKeyword(String keyword) { + SearchKeyword searchKeyword = SearchKeyword.builder().keyword(keyword).build(); + searchKeywordRepository.save(searchKeyword); + return searchKeyword; + } + + public SearchKeyword findOne(Long searchKeywordId) { + return searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND)); + } + + public Page findAll(Pageable pageable) { + return searchKeywordRepository.findAll(pageable); + } + + public SearchKeyword modifyOne(Long searchKeywordId, String keyword) { + SearchKeyword searchKeyword = searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND)); + searchKeyword.changeKeyword(keyword); + return searchKeyword; + } + + public void deleteOne(Long searchKeywordId) { + SearchKeyword searchKeyword = searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND)); + searchKeywordRepository.delete(searchKeyword); + } +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoApi.java b/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoApi.java index bdbb38e1..b92e9257 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoApi.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoApi.java @@ -1,13 +1,18 @@ package com.bbteam.budgetbuddies.domain.supportinfo.controller; import com.bbteam.budgetbuddies.apiPayload.ApiResponse; +import com.bbteam.budgetbuddies.apiPayload.code.ErrorReasonDto; import com.bbteam.budgetbuddies.domain.discountinfo.dto.DiscountResponseDto; import com.bbteam.budgetbuddies.domain.supportinfo.dto.SupportRequest; import com.bbteam.budgetbuddies.domain.supportinfo.dto.SupportResponseDto; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; import com.bbteam.budgetbuddies.domain.user.validation.ExistUser; +import com.bbteam.budgetbuddies.global.security.utils.AuthUser; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.PathVariable; @@ -18,10 +23,15 @@ public interface SupportInfoApi { @Operation(summary = "[User] 특정 년월 지원정보 리스트 가져오기 API", description = "특정 년도와 월에 해당하는 지원정보 목록을 조회하는 API이며, 페이징을 포함합니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) + }) @Parameters({ @Parameter(name = "year", description = "데이터를 가져올 연도입니다."), @@ -38,10 +48,13 @@ ApiResponse> getSupportsByYearAndMonth( @Operation(summary = "[ADMIN] 지원정보 등록하기 API", description = "지원정보를 등록하는 API이며, 추후에는 관리자만 접근 가능하도록 할 예정입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) ApiResponse registerSupportInfo( @RequestBody SupportRequest.RegisterSupportDto requestDto @@ -49,26 +62,33 @@ ApiResponse registerSupportInfo( @Operation(summary = "[User] 특정 지원정보에 좋아요 클릭 API", description = "특정 지원정보에 좋아요 버튼을 클릭하는 API이며, 일단은 사용자 ID를 입력하여 사용합니다. (추후 토큰으로 검증)") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "userId", description = "좋아요를 누른 사용자의 id입니다."), @Parameter(name = "supportInfoId", description = "좋아요를 누를 지원정보의 id입니다."), }) ApiResponse likeSupportInfo( - @RequestParam @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @PathVariable Long supportInfoId ); @Operation(summary = "[ADMIN] 특정 지원정보 수정하기 API", description = "ID를 통해 특정 지원정보를 수정하는 API입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ }) @@ -78,10 +98,13 @@ ApiResponse updateSupportInfo( @Operation(summary = "[ADMIN] 특정 지원정보 삭제하기 API", description = "ID를 통해 특정 지원정보를 삭제하는 API입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "supportInfoId", description = "삭제할 지원 정보의 id입니다."), @@ -92,10 +115,14 @@ ApiResponse deleteSupportInfo( @Operation(summary = "[ADMIN] 특정 지원정보 가져오기 API", description = "ID를 통해 특정 지원정보를 가져오는 API입니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "supportInfoId", description = "조회할 지원 정보의 id입니다."), @@ -106,10 +133,14 @@ ApiResponse getSupportInfo( @Operation(summary = "[ADMIN] 특정 사용자가 좋아요를 누른 지원정보 가져오기 API", description = "특정 사용자가 좋아요를 누른 지원정보들을 가져오는 API입니다. 페이징을 포함합니다.") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), -// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PAGE4001", description = "요청된 페이지가 0보다 작습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4001", description = "토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4002", description = "토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4003", description = "토큰이 만료되었습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4004", description = "토큰의 페이로드 혹은 시그니처가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "TOKEN4005", description = "토큰 헤더가 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON5000", description = "서버 에러. 관리자에게 문의하세요.", content = @Content(schema = @Schema(implementation = ErrorReasonDto.class))) }) @Parameters({ @Parameter(name = "userId", description = "특정 사용자의 id입니다."), @@ -117,7 +148,7 @@ ApiResponse getSupportInfo( @Parameter(name = "size", description = "한 페이지에 불러올 데이터 개수입니다. (기본값은 10개입니다.)") }) ApiResponse> getLikedSupportInfo( - @PathVariable @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size ); diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoController.java b/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoController.java index b40f9e81..ca04c776 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoController.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/supportinfo/controller/SupportInfoController.java @@ -5,7 +5,9 @@ import com.bbteam.budgetbuddies.domain.supportinfo.dto.SupportRequest; import com.bbteam.budgetbuddies.domain.supportinfo.dto.SupportResponseDto; import com.bbteam.budgetbuddies.domain.supportinfo.service.SupportInfoService; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; import com.bbteam.budgetbuddies.domain.user.validation.ExistUser; +import com.bbteam.budgetbuddies.global.security.utils.AuthUser; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; @@ -43,10 +45,10 @@ public ApiResponse registerSupportInfo( @Override @PostMapping("/likes/{supportInfoId}") public ApiResponse likeSupportInfo( - @RequestParam @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @PathVariable Long supportInfoId ) { - SupportResponseDto supportResponseDto = supportInfoService.toggleLike(userId, supportInfoId); + SupportResponseDto supportResponseDto = supportInfoService.toggleLike(user.getId(), supportInfoId); return ApiResponse.onSuccess(supportResponseDto); } @@ -84,11 +86,11 @@ public ApiResponse getSupportInfo( @Override @GetMapping("/liked-all") public ApiResponse> getLikedSupportInfo( - @RequestParam @ExistUser Long userId, + @AuthUser UserDto.AuthUserDto user, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size ) { - Page likedSupportInfoPage = supportInfoService.getLikedSupportInfo(userId, page, size); + Page likedSupportInfoPage = supportInfoService.getLikedSupportInfo(user.getId(), page, size); return ApiResponse.onSuccess(likedSupportInfoPage); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/user/dto/UserDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/user/dto/UserDto.java index 8bcc83e9..fe41208e 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/user/dto/UserDto.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/user/dto/UserDto.java @@ -52,4 +52,10 @@ public static class ModifyUserDto { private String email; private String name; } + + @Getter + @Builder + public static class AuthUserDto { + private Long id; + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/user/entity/User.java b/src/main/java/com/bbteam/budgetbuddies/domain/user/entity/User.java index f1baa2d3..4fc48bda 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/user/entity/User.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/user/entity/User.java @@ -23,39 +23,40 @@ @SuperBuilder public class User extends BaseEntity { - @Builder.Default - private Role role = Role.USER; // 기본값 User 권한 + @Builder.Default + private Role role = Role.USER; // 기본값 User 권한 - @Column(nullable = false, unique = true) - private String phoneNumber; + @Column(nullable = false, unique = true) + private String phoneNumber; - @Column(nullable = false, length = 20) - private String name; + @Column(nullable = false, length = 20) + private String name; - @Min(value = 1, message = "나이는 0또는 음수가 될 수 없습니다.") - private Integer age; + @Min(value = 1, message = "나이는 0또는 음수가 될 수 없습니다.") + private Integer age; - @Enumerated(EnumType.STRING) - @Column(columnDefinition = "varchar(20)") - private Gender gender; + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "varchar(20)") + private Gender gender; - @Column(nullable = false, length = 50, unique = true) - private String email; + @Column(nullable = false, length = 50, unique = true) + private String email; - @Column(nullable = true) - private String mobileCarrier; // 통신사 + @Column(nullable = true) + private String mobileCarrier; // 통신사 - @Column(nullable = true) - private String region; // 거주지 + @Column(nullable = true) + private String region; // 거주지 - private LocalDateTime lastLoginAt; + private LocalDateTime lastLoginAt; - public void changeUserDate(String email, String name) { - this.name = name; - this.email = email; - } - public List getAuthorities() { - return Collections.singletonList(new SimpleGrantedAuthority(role.name())); - } + public void changeUserDate(String email, String name) { + this.name = name; + this.email = email; + } + + public List getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority(role.name())); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserService.java b/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserService.java index f9e3ffa8..a4457ba9 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserService.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserService.java @@ -1,6 +1,7 @@ package com.bbteam.budgetbuddies.domain.user.service; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.UserConsumptionGoalResponse; import com.bbteam.budgetbuddies.domain.user.dto.UserDto; +import com.bbteam.budgetbuddies.domain.user.entity.User; import java.util.List; @@ -16,4 +17,6 @@ public interface UserService { UserDto.ResponseUserDto modifyUser(Long userId, UserDto.ModifyUserDto dto); List findAll(); + + User getUser(Long userId); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserServiceImpl.java index 8dbe683c..7fad61d6 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/user/service/UserServiceImpl.java @@ -108,4 +108,9 @@ public List findAll() { .map(UserConverter::toDto) .toList(); } + + @Override + public User getUser(Long userId) { + return userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Not found user")); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/global/config/CachingConfig.java b/src/main/java/com/bbteam/budgetbuddies/global/config/CachingConfig.java new file mode 100644 index 00000000..54ad8a20 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/global/config/CachingConfig.java @@ -0,0 +1,22 @@ +package com.bbteam.budgetbuddies.global.config; + +import java.util.List; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@EnableCaching +@Configuration +public class CachingConfig { + + @Bean + public CacheManager cacheManager() { + ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); + cacheManager.setAllowNullValues(false); + cacheManager.setCacheNames(List.of("consumptionMent")); + return cacheManager; + } +} diff --git a/src/main/java/com/bbteam/budgetbuddies/global/config/GeminiRestTemplateConfig.java b/src/main/java/com/bbteam/budgetbuddies/global/config/GeminiRestTemplateConfig.java deleted file mode 100644 index 294ba799..00000000 --- a/src/main/java/com/bbteam/budgetbuddies/global/config/GeminiRestTemplateConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.bbteam.budgetbuddies.global.config; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -import lombok.RequiredArgsConstructor; - -@Configuration -@RequiredArgsConstructor -public class GeminiRestTemplateConfig { - - @Bean - @Qualifier("geminiRestTemplate") - public RestTemplate geminiRestTemplate() { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add((request, body, execution) -> execution.execute(request, body)); - - return restTemplate; - } -} - diff --git a/src/main/java/com/bbteam/budgetbuddies/global/config/SecurityConfig.java b/src/main/java/com/bbteam/budgetbuddies/global/config/SecurityConfig.java index 56654536..a17564f3 100644 --- a/src/main/java/com/bbteam/budgetbuddies/global/config/SecurityConfig.java +++ b/src/main/java/com/bbteam/budgetbuddies/global/config/SecurityConfig.java @@ -21,45 +21,45 @@ public class SecurityConfig { - private final JwtRequestFilter jwtRequestFilter; + private final JwtRequestFilter jwtRequestFilter; - private final JwtExceptionFilter jwtExceptionFilter; + private final JwtExceptionFilter jwtExceptionFilter; - private final PhoneNumberAuthenticationProvider phoneNumberAuthenticationProvider; + private final PhoneNumberAuthenticationProvider phoneNumberAuthenticationProvider; - private final List swaggers = List.of( // Swagger 관련 URL 목록 - "/swagger-ui/**", - "/v3/api-docs/**" - ); + private final List swaggers = List.of( // Swagger 관련 URL 목록 + "/swagger-ui/**", + "/v3/api-docs/**" + ); - private final List auth = List.of( // 인증 관련 URL 목록 - "/auth/get-otp", - "/auth/login" - ); + private final List auth = List.of( // 인증 관련 URL 목록 + "/auth/get-otp", + "/auth/login" + ); - public SecurityConfig(JwtRequestFilter jwtRequestFilter, JwtExceptionFilter jwtExceptionFilter, PhoneNumberAuthenticationProvider phoneNumberAuthenticationProvider) { - this.jwtRequestFilter = jwtRequestFilter; - this.jwtExceptionFilter = jwtExceptionFilter; - this.phoneNumberAuthenticationProvider = phoneNumberAuthenticationProvider; - } + public SecurityConfig(JwtRequestFilter jwtRequestFilter, JwtExceptionFilter jwtExceptionFilter, PhoneNumberAuthenticationProvider phoneNumberAuthenticationProvider) { + this.jwtRequestFilter = jwtRequestFilter; + this.jwtExceptionFilter = jwtExceptionFilter; + this.phoneNumberAuthenticationProvider = phoneNumberAuthenticationProvider; + } - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) // csrf 설정 비활성화 - .authorizeHttpRequests(authorizeRequests -> - authorizeRequests - .requestMatchers(swaggers.toArray(new String[0])).permitAll() // 스웨거 주소 허용 - .requestMatchers(auth.toArray(new String[0])).permitAll() // 로그인 주소 허용 - .anyRequest().authenticated() // 그 외 모든 요청에 대해 인증 요구 - ) - .httpBasic(AbstractHttpConfigurer::disable) - .authenticationProvider(phoneNumberAuthenticationProvider) - .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(jwtExceptionFilter, JwtRequestFilter.class); // jwt 에러처리를 위한 필터등록 + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // csrf 설정 비활성화 + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests + .requestMatchers(swaggers.toArray(new String[0])).permitAll() // 스웨거 주소 허용 + .requestMatchers(auth.toArray(new String[0])).permitAll() // 로그인 주소 허용 + .anyRequest().authenticated() // 그 외 모든 요청에 대해 인증 요구 + ) + .httpBasic(AbstractHttpConfigurer::disable) + .authenticationProvider(phoneNumberAuthenticationProvider) + .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtExceptionFilter, JwtRequestFilter.class); // jwt 에러처리를 위한 필터등록 - return http.build(); - } + return http.build(); + } } \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/global/config/WebConfig.java b/src/main/java/com/bbteam/budgetbuddies/global/config/WebConfig.java new file mode 100644 index 00000000..712fb35a --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/global/config/WebConfig.java @@ -0,0 +1,21 @@ +package com.bbteam.budgetbuddies.global.config; + +import com.bbteam.budgetbuddies.global.security.utils.AuthArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final AuthArgumentResolver authArgumentResolver; + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(authArgumentResolver); + } +} \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/global/security/auth/controller/AuthenticationController.java b/src/main/java/com/bbteam/budgetbuddies/global/security/auth/controller/AuthenticationController.java index 2e63dca9..2d7a31fc 100644 --- a/src/main/java/com/bbteam/budgetbuddies/global/security/auth/controller/AuthenticationController.java +++ b/src/main/java/com/bbteam/budgetbuddies/global/security/auth/controller/AuthenticationController.java @@ -17,68 +17,68 @@ @RequestMapping("/auth") // 인증 관련 요청을 처리하는 컨트롤러 public class AuthenticationController implements AuthenticationApi { - private final AuthenticationService authenticationService; // 인증 관련 서비스 + private final AuthenticationService authenticationService; // 인증 관련 서비스 - /** - * OTP를 요청하는 엔드포인트. - * 전화번호를 입력받아 해당 번호로 OTP를 발송하고, 발송된 OTP 정보를 반환합니다. - * - * @param request OTP 요청에 필요한 전화번호 정보 - * @return 성공 시 전화번호와 생성된 OTP를 포함한 ApiResponse - */ - @PostMapping("/get-otp") - @Override - public ApiResponse getOtpNumber( - @Valid @RequestBody AuthenticationRequest.ToReceiveNumber request - ) { - String phoneNumber = request.getPhoneNumber(); // 요청에서 전화번호 추출 - OtpNumber generatedOtp = authenticationService.generateOtp(phoneNumber); // OTP 생성 - AuthenticationResponse.SendOtpNumber response = AuthenticationResponse.SendOtpNumber.builder() - .phoneNumber(phoneNumber) // 전화번호 설정 - .otpNumber(generatedOtp) // 생성된 OTP 정보 설정 - .build(); - return ApiResponse.onSuccess(response); // 성공 응답 반환 - } + /** + * OTP를 요청하는 엔드포인트. + * 전화번호를 입력받아 해당 번호로 OTP를 발송하고, 발송된 OTP 정보를 반환합니다. + * + * @param request OTP 요청에 필요한 전화번호 정보 + * @return 성공 시 전화번호와 생성된 OTP를 포함한 ApiResponse + */ + @PostMapping("/get-otp") + @Override + public ApiResponse getOtpNumber( + @Valid @RequestBody AuthenticationRequest.ToReceiveNumber request + ) { + String phoneNumber = request.getPhoneNumber(); // 요청에서 전화번호 추출 + OtpNumber generatedOtp = authenticationService.generateOtp(phoneNumber); // OTP 생성 + AuthenticationResponse.SendOtpNumber response = AuthenticationResponse.SendOtpNumber.builder() + .phoneNumber(phoneNumber) // 전화번호 설정 + .otpNumber(generatedOtp) // 생성된 OTP 정보 설정 + .build(); + return ApiResponse.onSuccess(response); // 성공 응답 반환 + } - /** - * 로그인 요청을 처리하는 엔드포인트. - * 전화번호와 OTP를 받아 로그인 처리 후, 인증 토큰을 반환합니다. - * - * @param request 로그인 요청에 필요한 전화번호 및 OTP 정보 - * @return 성공 시 인증 토큰 정보를 포함한 ApiResponse - */ - @PostMapping("/login") - @Override - public ApiResponse login( - @Valid @RequestBody AuthenticationRequest.ToLogin request - ) { - AuthenticationResponse.SendTokens response = authenticationService.login( - request.getPhoneNumber(), // 전화번호 추출 - request.getOtpNumber() // OTP 추출 - ); - return ApiResponse.onSuccess(response); // 성공 응답 반환 - } + /** + * 로그인 요청을 처리하는 엔드포인트. + * 전화번호와 OTP를 받아 로그인 처리 후, 인증 토큰을 반환합니다. + * + * @param request 로그인 요청에 필요한 전화번호 및 OTP 정보 + * @return 성공 시 인증 토큰 정보를 포함한 ApiResponse + */ + @PostMapping("/login") + @Override + public ApiResponse login( + @Valid @RequestBody AuthenticationRequest.ToLogin request + ) { + AuthenticationResponse.SendTokens response = authenticationService.login( + request.getPhoneNumber(), // 전화번호 추출 + request.getOtpNumber() // OTP 추출 + ); + return ApiResponse.onSuccess(response); // 성공 응답 반환 + } - /** - * 액세스 토큰 재발급 요청을 처리하는 엔드포인트. - * 현재 인증된 사용자로부터 새로운 액세스 토큰을 발급받습니다. - * - * @return 성공 시 새로 발급된 액세스 토큰 정보를 포함한 ApiResponse - */ - @GetMapping("/reissue-access-token") - @Override - public ApiResponse reIssueAccessToken() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 현재 인증 정보 가져오기 - User user = null; + /** + * 액세스 토큰 재발급 요청을 처리하는 엔드포인트. + * 현재 인증된 사용자로부터 새로운 액세스 토큰을 발급받습니다. + * + * @return 성공 시 새로 발급된 액세스 토큰 정보를 포함한 ApiResponse + */ + @GetMapping("/reissue-access-token") + @Override + public ApiResponse reIssueAccessToken() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 현재 인증 정보 가져오기 + User user = null; - // 인증된 사용자 정보가 있는지 확인 - if (authentication != null && authentication.isAuthenticated()) { - user = (User) authentication.getPrincipal(); // 인증된 사용자 추출 - } + // 인증된 사용자 정보가 있는지 확인 + if (authentication != null && authentication.isAuthenticated()) { + user = (User) authentication.getPrincipal(); // 인증된 사용자 추출 + } - // 새로운 액세스 토큰 발급 - AuthenticationResponse.SendAccessToken response = authenticationService.reIssueAccessToken(user); - return ApiResponse.onSuccess(response); // 성공 응답 반환 - } + // 새로운 액세스 토큰 발급 + AuthenticationResponse.SendAccessToken response = authenticationService.reIssueAccessToken(user); + return ApiResponse.onSuccess(response); // 성공 응답 반환 + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/global/security/auth/service/AuthenticationService.java b/src/main/java/com/bbteam/budgetbuddies/global/security/auth/service/AuthenticationService.java index 974f4b73..3976af3b 100644 --- a/src/main/java/com/bbteam/budgetbuddies/global/security/auth/service/AuthenticationService.java +++ b/src/main/java/com/bbteam/budgetbuddies/global/security/auth/service/AuthenticationService.java @@ -15,70 +15,70 @@ @RequiredArgsConstructor public class AuthenticationService { - private final JwtUtil jwtUtil; // JWT 관련 유틸리티 클래스 - private final OtpService otpService; // OTP 관련 서비스 클래스 - private final UserRepository userRepository; // 사용자 정보 저장소 + private final JwtUtil jwtUtil; // JWT 관련 유틸리티 클래스 + private final OtpService otpService; // OTP 관련 서비스 클래스 + private final UserRepository userRepository; // 사용자 정보 저장소 - /** - * OTP를 생성하여 반환하는 메서드. - * @param phoneNumber 전화번호 - * @return 생성된 OTP 정보 - */ - public OtpNumber generateOtp(String phoneNumber) { - // 전화번호를 사용하여 OTP를 생성하고 반환합니다. - return otpService.generateOtp(phoneNumber); - } + /** + * OTP를 생성하여 반환하는 메서드. + * @param phoneNumber 전화번호 + * @return 생성된 OTP 정보 + */ + public OtpNumber generateOtp(String phoneNumber) { + // 전화번호를 사용하여 OTP를 생성하고 반환합니다. + return otpService.generateOtp(phoneNumber); + } - /** - * 로그인 처리 메서드. - * 전화번호와 OTP를 검증한 후, 인증이 성공하면 JWT 토큰을 발급합니다. - * - * @param phoneNumber 전화번호 - * @param otpNumber 인증번호 - * @return 발급된 JWT 액세스 토큰과 리프레시 토큰 정보 - */ - public AuthenticationResponse.SendTokens login(String phoneNumber, String otpNumber) { - // 입력된 OTP가 유효한지 검증 - if (!otpService.validateOtp(phoneNumber, otpNumber)) { - throw new GeneralException(ErrorStatus._OTP_NOT_VALID); // 유효하지 않은 OTP일 경우 예외 발생 - } + /** + * 로그인 처리 메서드. + * 전화번호와 OTP를 검증한 후, 인증이 성공하면 JWT 토큰을 발급합니다. + * + * @param phoneNumber 전화번호 + * @param otpNumber 인증번호 + * @return 발급된 JWT 액세스 토큰과 리프레시 토큰 정보 + */ + public AuthenticationResponse.SendTokens login(String phoneNumber, String otpNumber) { + // 입력된 OTP가 유효한지 검증 + if (!otpService.validateOtp(phoneNumber, otpNumber)) { + throw new GeneralException(ErrorStatus._OTP_NOT_VALID); // 유효하지 않은 OTP일 경우 예외 발생 + } - // 전화번호로 사용자를 로드, 존재하지 않으면 새 사용자 생성 - final User user = userRepository.findFirstByPhoneNumber(phoneNumber) - .orElseGet(() -> userRepository.save(User.builder() // 사용자 정보가 없으면 새로 생성 - .phoneNumber(phoneNumber) - .build() - )); + // 전화번호로 사용자를 로드, 존재하지 않으면 새 사용자 생성 + final User user = userRepository.findFirstByPhoneNumber(phoneNumber) + .orElseGet(() -> userRepository.save(User.builder() // 사용자 정보가 없으면 새로 생성 + .phoneNumber(phoneNumber) + .build() + )); - // JWT 액세스 토큰 발급 - final String accessToken = jwtUtil.generateAccessToken(user); - // JWT 리프레시 토큰 발급 - final String refreshToken = jwtUtil.generateRefreshToken(user); + // JWT 액세스 토큰 발급 + final String accessToken = jwtUtil.generateAccessToken(user); + // JWT 리프레시 토큰 발급 + final String refreshToken = jwtUtil.generateRefreshToken(user); - // 발급된 토큰 정보를 포함한 응답 객체를 반환 - return AuthenticationResponse.SendTokens.builder() - .userId(user.getId()) // 사용자 ID - .phoneNumber(user.getPhoneNumber()) // 전화번호 - .accessToken(accessToken) // 액세스 토큰 - .refreshToken(refreshToken) // 리프레시 토큰 - .build(); - } + // 발급된 토큰 정보를 포함한 응답 객체를 반환 + return AuthenticationResponse.SendTokens.builder() + .userId(user.getId()) // 사용자 ID + .phoneNumber(user.getPhoneNumber()) // 전화번호 + .accessToken(accessToken) // 액세스 토큰 + .refreshToken(refreshToken) // 리프레시 토큰 + .build(); + } - /** - * 새로운 액세스 토큰을 재발급하는 메서드. - * - * @param user 사용자 정보 - * @return 발급된 새로운 액세스 토큰 정보 - */ - public AuthenticationResponse.SendAccessToken reIssueAccessToken(User user) { - // 새로운 액세스 토큰 발급 - String newAccessToken = jwtUtil.generateAccessToken(user); + /** + * 새로운 액세스 토큰을 재발급하는 메서드. + * + * @param user 사용자 정보 + * @return 발급된 새로운 액세스 토큰 정보 + */ + public AuthenticationResponse.SendAccessToken reIssueAccessToken(User user) { + // 새로운 액세스 토큰 발급 + String newAccessToken = jwtUtil.generateAccessToken(user); - // 발급된 새로운 토큰 정보를 포함한 응답 객체를 반환 - return AuthenticationResponse.SendAccessToken.builder() - .userId(user.getId()) // 사용자 ID - .phoneNumber(user.getPhoneNumber()) // 전화번호 - .accessToken(newAccessToken) // 새로운 액세스 토큰 - .build(); - } + // 발급된 새로운 토큰 정보를 포함한 응답 객체를 반환 + return AuthenticationResponse.SendAccessToken.builder() + .userId(user.getId()) // 사용자 ID + .phoneNumber(user.getPhoneNumber()) // 전화번호 + .accessToken(newAccessToken) // 새로운 액세스 토큰 + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/global/security/otp/OtpService.java b/src/main/java/com/bbteam/budgetbuddies/global/security/otp/OtpService.java index ff76eb7f..40c2eac3 100644 --- a/src/main/java/com/bbteam/budgetbuddies/global/security/otp/OtpService.java +++ b/src/main/java/com/bbteam/budgetbuddies/global/security/otp/OtpService.java @@ -66,7 +66,7 @@ public OtpNumber generateOtp(String phoneNumber) { otpCache.put(phoneNumber, otp); // 실제 메시지 전송 - sendMessage(phoneNumber, otp); + sendMessage(phoneNumber, otp); return otp; } @@ -85,7 +85,7 @@ public void sendMessage(String phoneNumber, OtpNumber otp) { message.setTo(phoneNumber); // 수신 번호 설정 // 메시지 내용 설정 (한글 45자 이하일 경우 자동으로 SMS로 전송) - message.setText("[빈주머니즈]\n인증번호: " + otp.getOtp()); + message.setText("[빈주머니즈]\n인증번호: " + otp.getOtp() + "\n보안을 위해 번호를 타인과 공유하지 마세요."); // 메시지 전송 요청 및 응답 로그 출력 SingleMessageSentResponse response = this.messageService.sendOne(new SingleMessageSendingRequest(message)); diff --git a/src/main/java/com/bbteam/budgetbuddies/global/security/utils/AuthArgumentResolver.java b/src/main/java/com/bbteam/budgetbuddies/global/security/utils/AuthArgumentResolver.java new file mode 100644 index 00000000..7560b0f2 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/global/security/utils/AuthArgumentResolver.java @@ -0,0 +1,47 @@ +package com.bbteam.budgetbuddies.global.security.utils; + +import com.bbteam.budgetbuddies.apiPayload.code.status.ErrorStatus; +import com.bbteam.budgetbuddies.apiPayload.exception.GeneralException; +import com.bbteam.budgetbuddies.domain.user.dto.UserDto; +import com.bbteam.budgetbuddies.domain.user.entity.User; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthArgumentResolver implements HandlerMethodArgumentResolver { + + // @Auth 존재 여부 확인 + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthUser.class); + } + + // @Auth 존재 시, 사용자 정보 확인하여 반환 + @Override + public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new GeneralException(ErrorStatus._USER_NOT_FOUND); + } + + Object principal = authentication.getPrincipal(); + if (!(principal instanceof User user)) { + throw new GeneralException(ErrorStatus._USER_NOT_FOUND); + } + + UserDto.AuthUserDto authUserDto = UserDto.AuthUserDto.builder() + .id(user.getId()) + .build(); + + return authUserDto; + } +} \ No newline at end of file diff --git a/src/main/java/com/bbteam/budgetbuddies/global/security/utils/AuthUser.java b/src/main/java/com/bbteam/budgetbuddies/global/security/utils/AuthUser.java new file mode 100644 index 00000000..9fa4a0e1 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/global/security/utils/AuthUser.java @@ -0,0 +1,12 @@ +package com.bbteam.budgetbuddies.global.security.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthUser { +} diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepositoryTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepositoryTest.java index 25b355f9..49634967 100644 --- a/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepositoryTest.java +++ b/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/repository/ConsumptionGoalRepositoryTest.java @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -61,6 +62,7 @@ void setUp() { .name("Peer User 1") .gender(Gender.MALE) .phoneNumber("010-1111-1111") + .mobileCarrier("SK Telecom") .build()); peerUser2 = userRepository.save( @@ -70,6 +72,7 @@ void setUp() { .name("Peer User 2") .gender(Gender.MALE) .phoneNumber("010-2222-2222") + .mobileCarrier("KT") .build()); currentMonth = LocalDate.now(); @@ -345,7 +348,8 @@ void findLatelyGoal_Success() { LocalDate searchDate = LocalDate.of(2024, 9, 1); // when - ConsumptionGoal result = consumptionGoalRepository.findLatelyGoal(user.getId(), category.getId(), searchDate).get(); + ConsumptionGoal result = consumptionGoalRepository.findLatelyGoal(user.getId(), category.getId(), searchDate) + .get(); // then assertEquals(result.getGoalMonth(), targetMonth); @@ -379,9 +383,44 @@ void findLatelyGoal_Success2() { .build()); // when - ConsumptionGoal result = consumptionGoalRepository.findLatelyGoal(user.getId(), category.getId(), targetMonth).get(); + ConsumptionGoal result = consumptionGoalRepository.findLatelyGoal(user.getId(), category.getId(), targetMonth) + .get(); // then assertEquals(result.getGoalMonth(), targetMonth); } + + @Test + @DisplayName("또래 나이, 성별, 카테고리로 최대 소비 금액 조회 성공") + void findMaxConsumeAmountByCategory_Success() { + // when + int peerAgeStart = 23; + int peerAgeEnd = 25; + Gender peerGender = Gender.MALE; + + Optional result = consumptionGoalRepository.findMaxConsumeAmountByCategory( + peerAgeStart, peerAgeEnd, peerGender, currentMonth); + + // then + assertThat(result).isPresent(); + assertThat(result.get().getConsumeAmount()).isEqualTo(150L); + assertThat(result.get().getCategory().getId()).isEqualTo(defaultCategory2.getId()); + } + + @Test + @DisplayName("또래 나이, 성별, 카테고리로 최대 목표 금액 조회 성공") + void findMaxGoalAmountByCategory_Success() { + // when + int peerAgeStart = 23; + int peerAgeEnd = 25; + Gender peerGender = Gender.MALE; + + Optional result = consumptionGoalRepository.findMaxGoalAmountByCategory( + peerAgeStart, peerAgeEnd, peerGender, currentMonth); + + // then + assertThat(result).isPresent(); + assertThat(result.get().getGoalAmount()).isEqualTo(200L); + assertThat(result.get().getCategory().getId()).isEqualTo(defaultCategory2.getId()); + } } \ No newline at end of file diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java index 49346396..a010109f 100644 --- a/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java +++ b/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java @@ -12,13 +12,12 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.Random; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; +import org.mockito.AdditionalAnswers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -27,6 +26,7 @@ import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; +import com.bbteam.budgetbuddies.domain.category.service.CategoryService; import com.bbteam.budgetbuddies.domain.consumptiongoal.converter.ConsumptionGoalConverter; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.AllConsumptionCategoryResponseDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.AvgConsumptionGoalDto; @@ -41,20 +41,18 @@ import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.TopCategoryConsumptionDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.entity.ConsumptionGoal; import com.bbteam.budgetbuddies.domain.consumptiongoal.repository.ConsumptionGoalRepository; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseUpdateRequestDto; -import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; +import com.bbteam.budgetbuddies.domain.user.service.UserService; import com.bbteam.budgetbuddies.enums.Gender; @DisplayName("ConsumptionGoalImpl 서비스 테스트의 ") @ExtendWith(MockitoExtension.class) class ConsumptionGoalServiceTest { - private final LocalDate GOAL_MONTH = LocalDate.of(2024, 07, 01); private final LocalDate currentMonth = LocalDate.now().withDayOfMonth(1); private User user; - private LocalDate goalMonthRandomDay; + private LocalDate requestMonth; @InjectMocks private ConsumptionGoalServiceImpl consumptionGoalService; @Mock @@ -62,17 +60,19 @@ class ConsumptionGoalServiceTest { @Mock private CategoryRepository categoryRepository; @Mock + private CategoryService categoryService; + @Mock private UserRepository userRepository; @Mock + private UserService userService; + @Mock private ExpenseRepository expenseRepository; @Spy private ConsumptionGoalConverter consumptionGoalConverter; @BeforeEach void setUp() { - Random random = new Random(); - int randomDay = random.nextInt(30) + 1; - goalMonthRandomDay = LocalDate.of(GOAL_MONTH.getYear(), GOAL_MONTH.getMonth(), randomDay); + requestMonth = LocalDate.of(2024, 7, 15); user = Mockito.spy(User.builder() .email("email") @@ -84,26 +84,23 @@ void setUp() { } @Test - @DisplayName("findUserConsumptionGoal : 생성된 ConsumptionGoal이 없고 카테고리만 있는 경우 목표 금액, 소비 금액 0으로 초기화") - void findUserConsumptionGoal_onlyCategory() { + void 유저소비목표조회시_이번달_이전달_소비목표가_없는_경우_소비목표를_새로_생성() { // given Category defaultCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").user(null).isDefault(true).build()); given(defaultCategory.getId()).willReturn(-1L); - Category userCategory = Mockito.spy(Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); given(userCategory.getId()).willReturn(-2L); - List categoryList = List.of(defaultCategory, userCategory); + given(categoryService.getUserCategoryList(user.getId())).willReturn(List.of(defaultCategory, userCategory)); + given(consumptionGoalRepository.save(any(ConsumptionGoal.class))).will(AdditionalAnswers.returnsFirstArg()); List expected = List.of( new ConsumptionGoalResponseDto(defaultCategory.getName(), defaultCategory.getId(), 0L, 0L), new ConsumptionGoalResponseDto(userCategory.getName(), userCategory.getId(), 0L, 0L)); // when - when(categoryRepository.findUserCategoryByUserId(user.getId())).thenReturn(categoryList); - ConsumptionGoalResponseListDto result = consumptionGoalService.findUserConsumptionGoalList(user.getId(), - goalMonthRandomDay); + requestMonth); // then assertThat(result.getConsumptionGoalList()).usingRecursiveComparison().isEqualTo(expected); @@ -111,117 +108,105 @@ void findUserConsumptionGoal_onlyCategory() { } @Test - @DisplayName("findUserConsumptionGoal : 한달전 ConsumptionGoal만 있을 경우 한달전으로 초기화") - void findUserConsumptionGoal_previousMonth() { + void 소비목표조회시_지난달_소비목표만_존재할시_지난달_기준으로_소비목표를_생성() { // given Category defaultCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").user(null).isDefault(true).build()); given(defaultCategory.getId()).willReturn(-1L); - Category userCategory = Mockito.spy(Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); given(userCategory.getId()).willReturn(-2L); - given(categoryRepository.findUserCategoryByUserId(user.getId())).willReturn( - List.of(defaultCategory, userCategory)); + given(categoryService.getUserCategoryList(user.getId())).willReturn(List.of(defaultCategory, userCategory)); + given(userService.getUser(user.getId())).willReturn(user); ConsumptionGoal previousMonthDefaultCategoryGoal = ConsumptionGoal.builder() .goalAmount(1_000_000L) .consumeAmount(200_000L) .user(user) .category(defaultCategory) - .goalMonth(goalMonthRandomDay.minusMonths(1)) + .goalMonth(requestMonth.withDayOfMonth(1).minusMonths(1)) .build(); - Long previousMonthDefaultGoalRemainingBalance = - previousMonthDefaultCategoryGoal.getGoalAmount() - previousMonthDefaultCategoryGoal.getConsumeAmount(); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, defaultCategory, + requestMonth.withDayOfMonth(1))).willReturn(Optional.empty()); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, defaultCategory, + requestMonth.withDayOfMonth(1).minusMonths(1))).willReturn(Optional.of(previousMonthDefaultCategoryGoal)); ConsumptionGoal previousMonthUserCategoryGoal = ConsumptionGoal.builder() .goalAmount(1_000_000L) .consumeAmount(20_000L) .user(user) .category(userCategory) - .goalMonth(goalMonthRandomDay.minusMonths(1)) + .goalMonth(requestMonth.withDayOfMonth(1).minusMonths(1)) .build(); - Long previousMonthUseGoalRemainingBalance = - previousMonthUserCategoryGoal.getGoalAmount() - previousMonthUserCategoryGoal.getConsumeAmount(); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, userCategory, + requestMonth.withDayOfMonth(1))).willReturn(Optional.empty()); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, userCategory, + requestMonth.withDayOfMonth(1).minusMonths(1))).willReturn(Optional.of(previousMonthUserCategoryGoal)); - List previousGoalList = List.of(previousMonthDefaultCategoryGoal, - previousMonthUserCategoryGoal); - - List expected = List.of( - consumptionGoalConverter.toConsumptionGoalResponseDto(previousMonthUserCategoryGoal), - consumptionGoalConverter.toConsumptionGoalResponseDto(previousMonthDefaultCategoryGoal)); + given(consumptionGoalRepository.save(any(ConsumptionGoal.class))).will(AdditionalAnswers.returnsFirstArg()); // when - when(consumptionGoalRepository.findConsumptionGoalByUserIdAndGoalMonth(user.getId(), - GOAL_MONTH.minusMonths(1))).thenReturn(previousGoalList); - ConsumptionGoalResponseListDto result = consumptionGoalService.findUserConsumptionGoalList(user.getId(), - goalMonthRandomDay); + requestMonth); // then - assertThat(result.getConsumptionGoalList()).usingRecursiveComparison().isEqualTo(expected); - assertEquals(result.getTotalRemainingBalance(), - previousMonthDefaultGoalRemainingBalance + previousMonthUseGoalRemainingBalance); + assertThat(result.getTotalRemainingBalance()).isEqualTo(2_000_000L); + assertThat(result.getTotalConsumptionAmount()).isEqualTo(0L); } @Test - @DisplayName("findUserConsumptionGoal : 한달 전과 목표 달 ConsumptionGoal이 있을 경우 목표 달로 초기화") - void findUserConsumptionGoal_previousMonthAndGoalMonth() { + void 소비목표_조회시_이번달_소비목표가_존재하는_경우_이번달_소비목표를_반환() { // given + Category defaultCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").user(null).isDefault(true).build()); + given(defaultCategory.getId()).willReturn(-1L); Category userCategory = Mockito.spy(Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); given(userCategory.getId()).willReturn(-2L); - ConsumptionGoal previousMonthUserCategoryGoal = ConsumptionGoal.builder() + given(categoryService.getUserCategoryList(user.getId())).willReturn(List.of(defaultCategory, userCategory)); + given(userService.getUser(user.getId())).willReturn(user); + + ConsumptionGoal thisMonthDefaultCategoryGoal = ConsumptionGoal.builder() .goalAmount(1_000_000L) - .consumeAmount(20_000L) + .consumeAmount(200_000L) .user(user) - .category(userCategory) - .goalMonth(goalMonthRandomDay.minusMonths(1)) + .category(defaultCategory) + .goalMonth(requestMonth.withDayOfMonth(1)) .build(); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, defaultCategory, + requestMonth.withDayOfMonth(1))).willReturn(Optional.of(thisMonthDefaultCategoryGoal)); - ConsumptionGoal goalMonthUserCategoryGoal = ConsumptionGoal.builder() - .goalAmount(2_000_000L) - .consumeAmount(30_000L) + ConsumptionGoal thisMonthUserCategoryGoal = ConsumptionGoal.builder() + .goalAmount(1_000_000L) + .consumeAmount(20_000L) .user(user) .category(userCategory) - .goalMonth(goalMonthRandomDay) + .goalMonth(requestMonth.withDayOfMonth(1)) .build(); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, userCategory, + requestMonth.withDayOfMonth(1))).willReturn(Optional.of(thisMonthUserCategoryGoal)); // when - when(consumptionGoalRepository.findConsumptionGoalByUserIdAndGoalMonth(user.getId(), - GOAL_MONTH.minusMonths(1))).thenReturn(List.of(previousMonthUserCategoryGoal)); - - when(consumptionGoalRepository.findConsumptionGoalByUserIdAndGoalMonth(user.getId(), GOAL_MONTH)).thenReturn( - List.of(goalMonthUserCategoryGoal)); - ConsumptionGoalResponseListDto result = consumptionGoalService.findUserConsumptionGoalList(user.getId(), - goalMonthRandomDay); + requestMonth); // then - assertThat(result.getConsumptionGoalList()).usingRecursiveComparison() - .isEqualTo(List.of(consumptionGoalConverter.toConsumptionGoalResponseDto(goalMonthUserCategoryGoal))); + assertThat(result.getTotalRemainingBalance()).isEqualTo(1_780_000L); + assertThat(result.getTotalConsumptionAmount()).isEqualTo(220_000L); } @Test - @DisplayName("updateConsumptionGoal : 이번달 목표가 있는 경우(defaultCategory)와 목표가 없는 경우(userCategory)") - void updateConsumptionGoal_Success() { + void 소비목표_업데이트_성공() { // given - Long defaultGoalAmount = 100L; - Long userGoalAmount = 200L; + Long updateAmount = 100L; LocalDate thisMonth = LocalDate.now().withDayOfMonth(1); - given(userRepository.findById(user.getId())).willReturn(Optional.ofNullable(user)); + given(userService.getUser(user.getId())).willReturn(user); ConsumptionGoalListRequestDto request = new ConsumptionGoalListRequestDto( - List.of(new ConsumptionGoalRequestDto(-1L, defaultGoalAmount), - new ConsumptionGoalRequestDto(-2L, userGoalAmount))); + List.of(new ConsumptionGoalRequestDto(-1L, updateAmount))); Category defaultCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").user(null).isDefault(true).build()); given(defaultCategory.getId()).willReturn(-1L); - given(categoryRepository.findById(defaultCategory.getId())).willReturn(Optional.of(defaultCategory)); - - Category userCategory = Mockito.spy(Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); - given(userCategory.getId()).willReturn(-2L); - given(categoryRepository.findById(userCategory.getId())).willReturn(Optional.of(userCategory)); + given(categoryService.getCategory(defaultCategory.getId())).willReturn(defaultCategory); ConsumptionGoal defaultCategoryGoal = ConsumptionGoal.builder() .goalAmount(1_000_000L) @@ -230,27 +215,14 @@ void updateConsumptionGoal_Success() { .category(defaultCategory) .goalMonth(thisMonth) .build(); - given(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, defaultCategory, - thisMonth)).willReturn(Optional.ofNullable(defaultCategoryGoal)); - - given(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, userCategory, - thisMonth)).willReturn(Optional.ofNullable(null)); - - when(consumptionGoalRepository.saveAll(any())).thenAnswer(invocation -> { - List goalsToSave = invocation.getArgument(0); - return goalsToSave; - }); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, defaultCategory, thisMonth)).willReturn( + Optional.ofNullable(defaultCategoryGoal)); List expected = List.of(ConsumptionGoalResponseDto.builder() - .goalAmount(defaultGoalAmount) + .goalAmount(updateAmount) .consumeAmount(defaultCategoryGoal.getConsumeAmount()) .categoryName(defaultCategory.getName()) .categoryId(defaultCategory.getId()) - .build(), ConsumptionGoalResponseDto.builder() - .goalAmount(userGoalAmount) - .consumeAmount(0L) - .categoryName(userCategory.getName()) - .categoryId(userCategory.getId()) .build()); // when @@ -302,11 +274,7 @@ void getPeerInfo_UserNotFound() { @DisplayName("getTopCategoryAndConsumptionAmount : 가장 큰 계획 카테고리와 이번 주 소비 금액 조회 성공") void getTopCategoryAndConsumptionAmount_Success() { // given - Category defaultCategory = Mockito.spy(Category.builder() - .name("디폴트 카테고리") - .user(null) - .isDefault(true) - .build()); + Category defaultCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").user(null).isDefault(true).build()); given(defaultCategory.getId()).willReturn(1L); LocalDate goalMonthRandomDay = LocalDate.now(); @@ -333,25 +301,21 @@ void getTopCategoryAndConsumptionAmount_Success() { .build(); List avgConsumptionGoalList = List.of( - new AvgConsumptionGoalDto(defaultCategory.getId(), 5000L) - ); + new AvgConsumptionGoalDto(defaultCategory.getId(), 5000L)); int peerAgeStart = 23; int peerAgeEnd = 25; Gender peerGender = Gender.MALE; given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - given(consumptionGoalRepository.findAvgGoalAmountByCategory( - peerAgeStart, peerAgeEnd, peerGender, currentMonth)) - .willReturn(avgConsumptionGoalList); + given(consumptionGoalRepository.findAvgGoalAmountByCategory(peerAgeStart, peerAgeEnd, peerGender, + currentMonth)).willReturn(avgConsumptionGoalList); - given(consumptionGoalRepository.findAvgConsumptionByCategoryIdAndCurrentWeek( - defaultCategory.getId(), startOfWeekDateTime, endOfWeekDateTime, - peerAgeStart, peerAgeEnd, peerGender)) - .willReturn(Optional.of(currentWeekConsumptionGoal.getConsumeAmount())); + given(consumptionGoalRepository.findAvgConsumptionByCategoryIdAndCurrentWeek(defaultCategory.getId(), + startOfWeekDateTime, endOfWeekDateTime, peerAgeStart, peerAgeEnd, peerGender)).willReturn( + Optional.of(currentWeekConsumptionGoal.getConsumeAmount())); - given(categoryRepository.findById(defaultCategory.getId())) - .willReturn(Optional.of(defaultCategory)); + given(categoryRepository.findById(defaultCategory.getId())).willReturn(Optional.of(defaultCategory)); // when ConsumptionAnalysisResponseDto result = consumptionGoalService.getTopCategoryAndConsumptionAmount(user.getId()); @@ -386,17 +350,17 @@ void getTopConsumptionCategories_Success() { String peerGender = "MALE"; given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - given(expenseRepository.findTopCategoriesByConsumptionCount(peerAgeStart, peerAgeEnd, - Gender.valueOf(peerGender), currentMonth.atStartOfDay())) - .willReturn(List.of(topConsumption1, topConsumption2, topConsumption3)); + given( + expenseRepository.findTopCategoriesByConsumptionCount(peerAgeStart, peerAgeEnd, Gender.valueOf(peerGender), + currentMonth.atStartOfDay())).willReturn(List.of(topConsumption1, topConsumption2, topConsumption3)); given(categoryRepository.findById(defaultCategory1.getId())).willReturn(Optional.of(defaultCategory1)); given(categoryRepository.findById(defaultCategory2.getId())).willReturn(Optional.of(defaultCategory2)); given(categoryRepository.findById(defaultCategory3.getId())).willReturn(Optional.of(defaultCategory3)); // when - List result = consumptionGoalService.getTopConsumptionCategories( - user.getId(), peerAgeStart, peerAgeEnd, peerGender); + List result = consumptionGoalService.getTopConsumptionCategories(user.getId(), + peerAgeStart, peerAgeEnd, peerGender); // then assertThat(result).hasSize(3); @@ -426,23 +390,19 @@ void getAllConsumptionGoalCategories_Success() { defaultCategory2.setName("디폴트 카테고리2"); defaultCategory2.setIsDefault(true); - List categoryAvgList = List.of( - new AvgConsumptionGoalDto(1L, 3000L), - new AvgConsumptionGoalDto(2L, 4000L) - ); + List categoryAvgList = List.of(new AvgConsumptionGoalDto(1L, 3000L), + new AvgConsumptionGoalDto(2L, 4000L)); - List myConsumptionAmountList = List.of( - new MyConsumptionGoalDto(1L, 5000L), - new MyConsumptionGoalDto(2L, 2000L) - ); + List myConsumptionAmountList = List.of(new MyConsumptionGoalDto(1L, 5000L), + new MyConsumptionGoalDto(2L, 2000L)); List defaultCategories = List.of(defaultCategory1, defaultCategory2); given(categoryRepository.findAllByIsDefaultTrue()).willReturn(defaultCategories); - given(consumptionGoalRepository.findAvgGoalAmountByCategory( - anyInt(), anyInt(), any(), any())).willReturn(categoryAvgList); - given(consumptionGoalRepository.findAllGoalAmountByUserId(user.getId(), currentMonth)) - .willReturn(myConsumptionAmountList); + given(consumptionGoalRepository.findAvgGoalAmountByCategory(anyInt(), anyInt(), any(), any())).willReturn( + categoryAvgList); + given(consumptionGoalRepository.findAllGoalAmountByUserId(user.getId(), currentMonth)).willReturn( + myConsumptionAmountList); given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); // when @@ -478,21 +438,18 @@ void getAllConsumptionCategories_Success() { defaultCategory2.setName("디폴트 카테고리2"); defaultCategory2.setIsDefault(true); - List categoryAvgList = List.of( - new AvgConsumptionGoalDto(1L, 3000L), - new AvgConsumptionGoalDto(2L, 4000L) - ); + List categoryAvgList = List.of(new AvgConsumptionGoalDto(1L, 3000L), + new AvgConsumptionGoalDto(2L, 4000L)); - List myConsumptionAmountList = List.of( - new MyConsumptionGoalDto(1L, 5000L), - new MyConsumptionGoalDto(2L, 2000L) - ); + List myConsumptionAmountList = List.of(new MyConsumptionGoalDto(1L, 5000L), + new MyConsumptionGoalDto(2L, 2000L)); List defaultCategories = List.of(defaultCategory1, defaultCategory2); given(categoryRepository.findAllByIsDefaultTrue()).willReturn(defaultCategories); - given(consumptionGoalRepository.findAvgConsumptionAmountByCategory( - anyInt(), anyInt(), any(), any())).willReturn(categoryAvgList); + given( + consumptionGoalRepository.findAvgConsumptionAmountByCategory(anyInt(), anyInt(), any(), any())).willReturn( + categoryAvgList); given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); // when @@ -515,166 +472,84 @@ void getAllConsumptionCategories_Success() { } @Test - @DisplayName("지난 달, 이번 달 소비 목표가 없는 카테고리에 대한 소비 업데이트를 진행하는 경우 새로운 소비 목표를 생성해 소비 금액을 갱신") - void recalculateConsumptionAmount_notExistPreviousMonthAndThisMonthGoal() { + void 이번달_사용자_소비목표_조회하기() { // given - Category existGoalCategory = Category.builder().name("유저 카테고리").user(user).isDefault(false).build(); - Category notExistGoalCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").isDefault(true).build()); - given(notExistGoalCategory.getId()).willReturn(-1L); - - Expense expense = Mockito.spy( - Expense.builder().category(existGoalCategory).expenseDate(GOAL_MONTH.atStartOfDay()).amount(1000L).build()); - when(expense.getId()).thenReturn(-1L); - - ExpenseUpdateRequestDto request = ExpenseUpdateRequestDto.builder() - .amount(1000L) - .expenseId(expense.getId()) - .expenseDate(LocalDate.of(2024, 8, 7).atStartOfDay()) - .categoryId(notExistGoalCategory.getId()) - .build(); + Category category = Mockito.spy(Category.builder().name("TEST CATEGORY").user(user).isDefault(false).build()); + LocalDate goalMonth = LocalDate.of(2024, 7, 1); - ConsumptionGoal oldGoal = ConsumptionGoal.builder().consumeAmount(1000L).category(existGoalCategory).build(); - ConsumptionGoal expected = ConsumptionGoal.builder() - .goalMonth(LocalDate.of(2024, 8, 1)) - .goalAmount(0L) - .consumeAmount(1000L) - .category(notExistGoalCategory) + ConsumptionGoal userConsumptionGoal = ConsumptionGoal.builder() + .goalAmount(1_000_000L) + .consumeAmount(200_000L) .user(user) + .category(category) + .goalMonth(goalMonth) .build(); - // when - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, expense.getCategory(), - expense.getExpenseDate().toLocalDate().withDayOfMonth(1))).thenReturn(Optional.ofNullable(oldGoal)); - - when(categoryRepository.findById(request.getCategoryId())).thenReturn(Optional.of(notExistGoalCategory)); - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, notExistGoalCategory, - request.getExpenseDate().toLocalDate().withDayOfMonth(1))).thenReturn(Optional.empty()); - - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, notExistGoalCategory, - request.getExpenseDate().minusMonths(1).toLocalDate().withDayOfMonth(1))).thenReturn(Optional.empty()); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, goalMonth)).willReturn( + Optional.of(userConsumptionGoal)); - consumptionGoalService.recalculateConsumptionAmount(expense, request, user); - - ArgumentCaptor consumptionGoalCaptor = ArgumentCaptor.forClass(ConsumptionGoal.class); - verify(consumptionGoalRepository, times(2)).save(consumptionGoalCaptor.capture()); - - List savedConsumptionGoals = consumptionGoalCaptor.getAllValues(); + // when + ConsumptionGoal result = consumptionGoalService.getUserConsumptionGoal(user, category, goalMonth); // then - assertEquals(oldGoal.getConsumeAmount(), 0L); - assertThat(savedConsumptionGoals.get(1)).usingRecursiveComparison().isEqualTo(expected); + assertEquals(result, userConsumptionGoal); } @Test - @DisplayName("이번달 소비 목표가 없는 카테고리에 대한 소비 업데이트를 진행하는 경우 지난 달 소비 목표의 목표 금액을 복사한 이번 달 소비 목표를 생성해 소비 금액을 갱신") - void recalculateConsumptionAmount_notExistThisMonthGoal() { + void 이번달_소비목표가_없는경우_저번달_기준으로_소비목표_생성() { // given - Category existGoalCategory = Category.builder().name("유저 카테고리").user(user).isDefault(false).build(); - Category notExistThisMonthGoalCategory = Mockito.spy( - Category.builder().name("디폴트 카테고리").isDefault(true).build()); - given(notExistThisMonthGoalCategory.getId()).willReturn(-1L); - - Expense expense = Mockito.spy( - Expense.builder().category(existGoalCategory).expenseDate(GOAL_MONTH.atStartOfDay()).amount(1000L).build()); - when(expense.getId()).thenReturn(-1L); - - ExpenseUpdateRequestDto request = ExpenseUpdateRequestDto.builder() - .amount(1000L) - .expenseId(expense.getId()) - .expenseDate(LocalDate.of(2024, 8, 7).atStartOfDay()) - .categoryId(notExistThisMonthGoalCategory.getId()) - .build(); + Category category = Mockito.spy(Category.builder().name("TEST CATEGORY").user(user).isDefault(false).build()); + LocalDate goalMonth = LocalDate.of(2024, 7, 1); - ConsumptionGoal oldGoal = ConsumptionGoal.builder().consumeAmount(1000L).category(existGoalCategory).build(); - - ConsumptionGoal previousMonthGoal = ConsumptionGoal.builder() - .goalMonth(LocalDate.of(2024, 7, 1)) - .goalAmount(3000L) - .consumeAmount(3000L) - .category(notExistThisMonthGoalCategory) + ConsumptionGoal beforeUserConsumptionGoal = ConsumptionGoal.builder() + .goalAmount(1_000_000L) + .consumeAmount(200_000L) .user(user) + .category(category) + .goalMonth(LocalDate.of(2024, 6, 1)) .build(); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, goalMonth)).willReturn( + Optional.empty()); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, + goalMonth.minusMonths(1))).willReturn(Optional.of(beforeUserConsumptionGoal)); + given(consumptionGoalRepository.save(any(ConsumptionGoal.class))).will(AdditionalAnswers.returnsFirstArg()); ConsumptionGoal expected = ConsumptionGoal.builder() - .goalMonth(LocalDate.of(2024, 8, 1)) - .goalAmount(3000L) - .consumeAmount(1000L) - .category(notExistThisMonthGoalCategory) + .goalAmount(1_000_000L) + .consumeAmount(0L) .user(user) + .category(category) + .goalMonth(LocalDate.of(2024, 7, 1)) .build(); - // when - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, expense.getCategory(), - expense.getExpenseDate().toLocalDate().withDayOfMonth(1))).thenReturn(Optional.ofNullable(oldGoal)); - - when(categoryRepository.findById(request.getCategoryId())).thenReturn( - Optional.of(notExistThisMonthGoalCategory)); - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, - notExistThisMonthGoalCategory, request.getExpenseDate().toLocalDate().withDayOfMonth(1))).thenReturn( - Optional.empty()); - - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, - notExistThisMonthGoalCategory, - request.getExpenseDate().minusMonths(1).toLocalDate().withDayOfMonth(1))).thenReturn( - Optional.ofNullable(previousMonthGoal)); - - consumptionGoalService.recalculateConsumptionAmount(expense, request, user); - - ArgumentCaptor consumptionGoalCaptor = ArgumentCaptor.forClass(ConsumptionGoal.class); - verify(consumptionGoalRepository, times(2)).save(consumptionGoalCaptor.capture()); - - List savedConsumptionGoals = consumptionGoalCaptor.getAllValues(); + ConsumptionGoal result = consumptionGoalService.getUserConsumptionGoal(user, category, goalMonth); // then - assertEquals(oldGoal.getConsumeAmount(), 0L); - assertThat(savedConsumptionGoals.get(1)).usingRecursiveComparison().isEqualTo(expected); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); } @Test - @DisplayName("이번달 소비 목표가 있는 경우 이번 달 소비 목표의 소비 금액을 갱신") - void recalculateConsumptionAmount_existThisMonthGoal() { + void 이번달_저번달_소비목표가_없는경우_소비목표를_새로_생성() { // given - Category existGoalCategory = Mockito.spy( - Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); - given(existGoalCategory.getId()).willReturn(-1L); - - Expense expense = Mockito.spy( - Expense.builder().category(existGoalCategory).expenseDate(GOAL_MONTH.atStartOfDay()).amount(1000L).build()); - when(expense.getId()).thenReturn(-1L); - - ExpenseUpdateRequestDto request = ExpenseUpdateRequestDto.builder() - .amount(2000L) - .expenseId(expense.getId()) - .expenseDate(LocalDate.of(2024, 8, 7).atStartOfDay()) - .categoryId(existGoalCategory.getId()) - .build(); + Category category = Mockito.spy(Category.builder().name("TEST CATEGORY").user(user).isDefault(false).build()); + LocalDate goalMonth = LocalDate.of(2024, 7, 1); - ConsumptionGoal oldGoal = ConsumptionGoal.builder() - .goalMonth(LocalDate.of(2024, 8, 1)) - .goalAmount(3000L) - .consumeAmount(1000L) - .category(existGoalCategory) - .user(user) - .build(); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, goalMonth)).willReturn( + Optional.empty()); + given(consumptionGoalRepository.findByUserAndCategoryAndGoalMonth(user, category, + goalMonth.minusMonths(1))).willReturn(Optional.empty()); + given(consumptionGoalRepository.save(any(ConsumptionGoal.class))).will(AdditionalAnswers.returnsFirstArg()); ConsumptionGoal expected = ConsumptionGoal.builder() - .goalMonth(LocalDate.of(2024, 8, 1)) - .goalAmount(3000L) - .consumeAmount(2000L) - .category(existGoalCategory) + .goalAmount(0L) + .consumeAmount(0L) .user(user) + .category(category) + .goalMonth(LocalDate.of(2024, 7, 1)) .build(); - // when - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, expense.getCategory(), - expense.getExpenseDate().toLocalDate().withDayOfMonth(1))).thenReturn(Optional.ofNullable(oldGoal)); - - when(categoryRepository.findById(request.getCategoryId())).thenReturn(Optional.of(existGoalCategory)); - when(consumptionGoalRepository.findConsumptionGoalByUserAndCategoryAndGoalMonth(user, existGoalCategory, - request.getExpenseDate().toLocalDate().withDayOfMonth(1))).thenReturn(Optional.ofNullable(oldGoal)); - - consumptionGoalService.recalculateConsumptionAmount(expense, request, user); + ConsumptionGoal result = consumptionGoalService.getUserConsumptionGoal(user, category, goalMonth); // then - assertThat(oldGoal).usingRecursiveComparison().isEqualTo(expected); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); } } \ No newline at end of file diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java index c1b34ada..97806cd2 100644 --- a/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java +++ b/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.*; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -30,59 +29,63 @@ class ExpenseRepositoryTest { private CategoryRepository categoryRepository; @Test - @DisplayName("findAllByUserIdFOrPeriod 성공") - void findAllByUserIdForPeriod_Success() { + void 월별_소비조회_월초_월말_조회_성공() { // given - User user = userRepository.save( - User.builder().email("email").age(24).name("name").phoneNumber("010-1234-5678").build()); + User user = userRepository.save(User.builder() + .email("email") + .age(24) + .name("test user") + .phoneNumber("010-1234-5678") + .mobileCarrier("TEST") + .build()); Category userCategory = categoryRepository.save( Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); - LocalDate startDate = LocalDate.of(2024, 7, 1); - LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth()); + LocalDate startMonth = LocalDate.of(2024, 7, 1); + LocalDate nextMonth = startMonth.plusMonths(1L); - List expected = setExpense(user, userCategory, startDate); + List expected = getExpectedExpense(user, userCategory); + setUnExpectedExpense(user, userCategory, nextMonth); // when - List result = expenseRepository.findAllByUserIdForPeriod(user, - startDate.atStartOfDay(), endDate.atStartOfDay()); + List result = expenseRepository.findAllByUserIdForPeriod(user.getId(), startMonth.atStartOfDay(), + nextMonth.atStartOfDay()); assertThat(result).usingRecursiveComparison().isEqualTo(expected); } - private List setExpense(User user, Category userCategory, LocalDate startDate) { - setUnexpectedExpense(user, userCategory, startDate); - - return setExpectedExpenseOrderByDateDesc(user, userCategory, startDate); - } - - private List setExpectedExpenseOrderByDateDesc(User user, Category userCategory, LocalDate startDate) { - List expenses = new ArrayList<>(); - - for (int i = startDate.lengthOfMonth(); i > startDate.lengthOfMonth() - 5; i--) { - Expense expense = Expense.builder() - .user(user) - .category(userCategory) - .amount(100000L * i) - .expenseDate(startDate.withDayOfMonth(i).atStartOfDay()) - .build(); - - expenses.add(expenseRepository.save(expense)); - } - return expenses; + private List getExpectedExpense(User user, Category userCategory) { + Expense monthOfStartExpense = Expense.builder() + .user(user) + .category(userCategory) + .amount(100000L) + .expenseDate(LocalDate.of(2024, 7, 1).atStartOfDay()) + .build(); + + Expense monthOfLastExpense = Expense.builder() + .user(user) + .category(userCategory) + .amount(100000L) + .expenseDate(LocalDate.of(2024, 7, 31).atTime(23, 59)) + .build(); + + return List.of(expenseRepository.save(monthOfLastExpense), expenseRepository.save(monthOfStartExpense)); } - private void setUnexpectedExpense(User user, Category userCategory, LocalDate startDate) { - for (int i = 1; i <= 5; i++) { - Expense expense = Expense.builder() - .user(user) - .category(userCategory) - .amount(100000L * i) - .expenseDate(startDate.withMonth(8).atStartOfDay()) - .build(); - - expenseRepository.save(expense); - } + private void setUnExpectedExpense(User user, Category userCategory, LocalDate nextMonth) { + expenseRepository.save(Expense.builder() + .user(user) + .category(userCategory) + .amount(100000L) + .expenseDate(LocalDate.of(2024, 8, 1).atStartOfDay()) + .build()); + + expenseRepository.save(Expense.builder() + .user(user) + .category(userCategory) + .amount(100000L) + .expenseDate(LocalDate.of(2024, 6, 30).atTime(23, 59)) + .build()); } } \ No newline at end of file diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java index 2c7a8484..3de8bbed 100644 --- a/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java +++ b/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java @@ -24,7 +24,7 @@ import com.bbteam.budgetbuddies.domain.expense.converter.ExpenseConverter; import com.bbteam.budgetbuddies.domain.expense.dto.CompactExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.DailyExpenseResponseDto; -import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.DetailExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; @@ -54,11 +54,8 @@ void setUp() { } @Test - @DisplayName("월별 소비 조회 소비를 DailyExpenseResponseDto로 반환") - void getMonthlyExpense_Success() { + void 월별_소비목록_조회_소비일을_기준으로_소비를_반환_성공() { // given - given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - Category userCategory = Mockito.spy(Category.builder().build()); given(userCategory.getId()).willReturn(-1L); @@ -66,32 +63,10 @@ void getMonthlyExpense_Success() { List expenses = generateExpenseList(requestMonth, user, userCategory); - given(expenseRepository.findAllByUserIdForPeriod(any(User.class), any(LocalDateTime.class), + given(expenseRepository.findAllByUserIdForPeriod(anyLong(), any(LocalDateTime.class), any(LocalDateTime.class))).willReturn(expenses); - MonthlyExpenseResponseDto expected = MonthlyExpenseResponseDto.builder() - .expenseMonth(LocalDate.of(2024, 07, 01)) - .totalConsumptionAmount(300_000L) - .dailyExpenses(List.of(DailyExpenseResponseDto.builder() - .daysOfMonth(2) - .daysOfTheWeek("화요일") - .expenses(List.of(CompactExpenseResponseDto.builder() - .amount(200_000L) - .description("User 소비") - .expenseId(-2L) - .categoryId(userCategory.getId()) - .build())) - .build(), DailyExpenseResponseDto.builder() - .daysOfMonth(1) - .daysOfTheWeek("월요일") - .expenses(List.of(CompactExpenseResponseDto.builder() - .amount(100_000L) - .description("User 소비") - .expenseId(-1L) - .categoryId(userCategory.getId()) - .build())) - .build())) - .build(); + MonthlyExpenseResponseDto expected = getExpectedMonthlyExpense(userCategory); // when MonthlyExpenseResponseDto result = expenseService.getMonthlyExpense(user.getId(), requestMonth); @@ -100,6 +75,35 @@ void getMonthlyExpense_Success() { assertThat(result).usingRecursiveComparison().isEqualTo(expected); } + private MonthlyExpenseResponseDto getExpectedMonthlyExpense(Category userCategory) { + return MonthlyExpenseResponseDto.builder() + .expenseMonth(LocalDate.of(2024, 7, 1)) + .totalConsumptionAmount(300_000L) + .dailyExpenses( + List.of( + DailyExpenseResponseDto.builder() + .daysOfMonth(2) + .daysOfTheWeek("화요일") + .expenses(List.of(CompactExpenseResponseDto.builder() + .amount(200_000L) + .description("User 소비") + .expenseId(-2L) + .categoryId(userCategory.getId()) + .build())) + .build(), + DailyExpenseResponseDto.builder() + .daysOfMonth(1) + .daysOfTheWeek("월요일") + .expenses(List.of(CompactExpenseResponseDto.builder() + .amount(100_000L) + .description("User 소비") + .expenseId(-1L) + .categoryId(userCategory.getId()) + .build())) + .build())) + .build(); + } + private List generateExpenseList(LocalDate month, User user, Category userCategory) { Expense e1 = Mockito.spy(Expense.builder() .amount(100_000L) @@ -123,8 +127,7 @@ private List generateExpenseList(LocalDate month, User user, Category u } @Test - @DisplayName("findExpenseFromUserIdAndExpenseId : 성공") - void findExpenseResponseFromUserIdAndExpenseId_Success() { + void 소비상세조회_성공() { // given final Long expenseId = -1L; @@ -135,8 +138,7 @@ void findExpenseResponseFromUserIdAndExpenseId_Success() { given(expense.getId()).willReturn(expenseId); given(expenseRepository.findById(expense.getId())).willReturn(Optional.of(expense)); - ExpenseResponseDto expected = ExpenseResponseDto.builder() - .userId(user.getId()) + DetailExpenseResponseDto expected = DetailExpenseResponseDto.builder() .expenseId(-1L) .description("유저 소비") .categoryName("유저 카테고리") @@ -144,15 +146,14 @@ void findExpenseResponseFromUserIdAndExpenseId_Success() { .build(); // when - ExpenseResponseDto result = expenseService.findExpenseResponseFromUserIdAndExpenseId(user.getId(), expenseId); + DetailExpenseResponseDto result = expenseService.findDetailExpenseResponse(user.getId(), expenseId); // then assertThat(result).usingRecursiveComparison().isEqualTo(expected); } @Test - @DisplayName("findExpenseFromUserIdAndExpenseId : 소비 유저와 다른 유저로 인한 예외 반환") - void findExpenseResponseFromUserIdAndExpenseId_Fail() { + void 조회_권한이_없는_유저의_소비조회로_인한_예외_반환() { // given final Long expenseId = -1L; @@ -164,6 +165,6 @@ void findExpenseResponseFromUserIdAndExpenseId_Fail() { // then assertThrows(IllegalArgumentException.class, - () -> expenseService.findExpenseResponseFromUserIdAndExpenseId(-2L, expenseId)); + () -> expenseService.findDetailExpenseResponse(-2L, expenseId)); } } \ No newline at end of file diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepositoryTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepositoryTest.java new file mode 100644 index 00000000..6263fd38 --- /dev/null +++ b/src/test/java/com/bbteam/budgetbuddies/domain/faq/repository/FaqRepositoryTest.java @@ -0,0 +1,101 @@ +package com.bbteam.budgetbuddies.domain.faq.repository; + +import com.bbteam.budgetbuddies.domain.faq.entity.Faq; +import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword; +import com.bbteam.budgetbuddies.domain.faqkeyword.repository.FaqKeywordRepository; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import com.bbteam.budgetbuddies.domain.searchkeyword.repository.SearchKeywordRepository; +import com.bbteam.budgetbuddies.domain.user.entity.User; +import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class FaqRepositoryTest { + @Autowired + FaqRepository faqRepository; + @Autowired + FaqKeywordRepository faqKeywordRepository; + @Autowired + SearchKeywordRepository searchKeywordRepository; + @Autowired + UserRepository userRepository; + + + @Test + void searchTest() { + + User user1 = User.builder() + .name("tester1") + .email("1234") + .age(5) + .phoneNumber("123456") + .mobileCarrier("skt") + .build(); + userRepository.save(user1); + + Faq faq1 = Faq.builder() + .user(user1) + .title("test1") + .body("test1") + .build(); + faqRepository.save(faq1); + + Faq faq2 = Faq.builder() + .user(user1) + .title("test2") + .body("test2") + .build(); + faqRepository.save(faq2); + + SearchKeyword keyword1 = SearchKeyword.builder() + .keyword("러닝") + .build(); + searchKeywordRepository.save(keyword1); + + FaqKeyword faqKeyword = FaqKeyword.builder() + .searchKeyword(keyword1) + .faq(faq1) + .build(); + faqKeywordRepository.save(faqKeyword); + + SearchKeyword keyword2 = SearchKeyword.builder() + .keyword("헬스") + .build(); + searchKeywordRepository.save(keyword2); + + FaqKeyword faqKeyword2 = FaqKeyword.builder() + .searchKeyword(keyword2) + .faq(faq1) + .build(); + faqKeywordRepository.save(faqKeyword2); + + FaqKeyword faqKeyword3 = FaqKeyword.builder() + .searchKeyword(keyword1) + .faq(faq2) + .build(); + faqKeywordRepository.save(faqKeyword3); + + PageRequest pageRequest = PageRequest.of(0, 5); + + Page result1 = faqRepository.searchFaq(pageRequest, "러닝"); + + Page result2 = faqRepository.searchFaq(pageRequest, "헬스"); + + assertThat(result1.getNumberOfElements()).isEqualTo(2); + assertThat(result2.getNumberOfElements()).isEqualTo(1); + + } + + +} \ No newline at end of file diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceTest.java index 4e895c18..67d5e7b1 100644 --- a/src/test/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceTest.java +++ b/src/test/java/com/bbteam/budgetbuddies/domain/faq/service/FaqServiceTest.java @@ -4,6 +4,10 @@ import com.bbteam.budgetbuddies.domain.faq.dto.FaqResponseDto; import com.bbteam.budgetbuddies.domain.faq.entity.Faq; import com.bbteam.budgetbuddies.domain.faq.repository.FaqRepository; +import com.bbteam.budgetbuddies.domain.faqkeyword.dto.FaqKeywordResponseDto; +import com.bbteam.budgetbuddies.domain.faqkeyword.repository.FaqKeywordRepository; +import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword; +import com.bbteam.budgetbuddies.domain.searchkeyword.repository.SearchKeywordRepository; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; import org.assertj.core.api.Assertions; @@ -14,6 +18,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -31,6 +36,10 @@ class FaqServiceTest { FaqRepository faqRepository; @Autowired UserRepository userRepository; + @Autowired + FaqKeywordRepository faqKeywordRepository; + @Autowired + SearchKeywordRepository searchKeywordRepository; static Long userId; static Long faqId; @@ -149,4 +158,35 @@ void deleteFaq() { assertThat(faq.isEmpty()).isTrue(); } + + @Test + void addKeywordAndRemoveKeyword() { + User user = userRepository.findById(userId).get(); + + Faq faq1 = Faq.builder() + .title("test1") + .body("test1") + .user(user) + .build(); + faqRepository.save(faq1); + + SearchKeyword searchKeyword = SearchKeyword.builder() + .keyword("testKeyword") + .build(); + searchKeywordRepository.save(searchKeyword); + faqService.addKeyword(faq1.getId(), searchKeyword.getId()); + PageRequest pageRequest = PageRequest.of(0, 1); + + Page result1 = faqService.searchFaq(pageRequest, "test"); + Assertions.assertThat(result1.getNumberOfElements()).isEqualTo(1); + + Page result2 = faqService.searchFaq(pageRequest, "no"); + Assertions.assertThat(result2.getNumberOfElements()).isEqualTo(0); + + faqService.removeKeyword(faq1.getId(), searchKeyword.getId()); + Page result3 = faqService.searchFaq(pageRequest, "test"); + Assertions.assertThat(result3.getNumberOfElements()).isEqualTo(0); + + + } } \ No newline at end of file