diff --git a/src/main/java/sopt/org/hmh/domain/admin/controller/AdminApi.java b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminApi.java new file mode 100644 index 00000000..51b57e3f --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminApi.java @@ -0,0 +1,24 @@ +package sopt.org.hmh.domain.admin.controller; + +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.ResponseEntity; +import sopt.org.hmh.domain.admin.dto.request.AdminDailyChallengeRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminLoginRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserIdRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserInfoRequest; +import sopt.org.hmh.domain.admin.dto.response.AdminTokenResponse; +import sopt.org.hmh.global.common.response.BaseResponse; + +public interface AdminApi { + @Operation(summary = "관리자 로그인") + ResponseEntity> orderAdminLogin(AdminLoginRequest request); + + @Operation(summary = "관리자 권한으로 유저 즉시 삭제") + ResponseEntity orderAdminWithdrawImmediately(AdminUserIdRequest request); + + @Operation(summary = "관리자 권한으로 유저 정보 변경") + ResponseEntity orderAdminChangeUserInfo(AdminUserInfoRequest request); + + @Operation(summary = "관리자 권한으로 유저 챌린지 정보 변경") + ResponseEntity orderAdminChangeDailyChallengeInfo(AdminDailyChallengeRequest request); +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/controller/AdminController.java b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminController.java new file mode 100644 index 00000000..647cea23 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminController.java @@ -0,0 +1,67 @@ +package sopt.org.hmh.domain.admin.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sopt.org.hmh.domain.admin.dto.request.AdminDailyChallengeRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminLoginRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserIdRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserInfoRequest; +import sopt.org.hmh.domain.admin.dto.response.AdminTokenResponse; +import sopt.org.hmh.domain.admin.exception.AdminSuccess; +import sopt.org.hmh.domain.admin.service.AdminFacade; +import sopt.org.hmh.global.common.response.BaseResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/admin") +public class AdminController implements AdminApi { + + private final AdminFacade adminFacade; + + @Override + @PostMapping("/login") + public ResponseEntity> orderAdminLogin( + @RequestBody final AdminLoginRequest request) { + return ResponseEntity + .status(AdminSuccess.ADMIN_LOGIN_SUCCESS.getHttpStatus()) + .body(BaseResponse.success(AdminSuccess.ADMIN_LOGIN_SUCCESS, + adminFacade.adminLogin(request.authCode()))); + } + + @Override + @DeleteMapping("/user") + public ResponseEntity orderAdminWithdrawImmediately( + @RequestBody @Valid final AdminUserIdRequest request) { + adminFacade.withdrawImmediately(request.userId()); + return ResponseEntity + .noContent() + .build(); + } + + @Override + @PatchMapping("/user") + public ResponseEntity orderAdminChangeUserInfo( + @RequestBody @Valid final AdminUserInfoRequest request) { + adminFacade.changeUserInfo(request); + return ResponseEntity + .noContent() + .build(); + } + + @Override + @PatchMapping("/challenge/daily") + public ResponseEntity orderAdminChangeDailyChallengeInfo( + @RequestBody @Valid final AdminDailyChallengeRequest request) { + adminFacade.changeDailyChallengeInfo(request); + return ResponseEntity + .noContent() + .build(); + } +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminDailyChallengeRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminDailyChallengeRequest.java new file mode 100644 index 00000000..ccd4e389 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminDailyChallengeRequest.java @@ -0,0 +1,12 @@ +package sopt.org.hmh.domain.admin.dto.request; + +import java.time.LocalDate; +import java.util.List; +import sopt.org.hmh.domain.dailychallenge.domain.Status; + +public record AdminDailyChallengeRequest( + Long userId, + LocalDate startDate, + List statuses +) { +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminLoginRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminLoginRequest.java new file mode 100644 index 00000000..a280bdab --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminLoginRequest.java @@ -0,0 +1,7 @@ +package sopt.org.hmh.domain.admin.dto.request; + +public record AdminLoginRequest( + String authCode +) { + +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserIdRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserIdRequest.java new file mode 100644 index 00000000..c1d513c3 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserIdRequest.java @@ -0,0 +1,7 @@ +package sopt.org.hmh.domain.admin.dto.request; + +public record AdminUserIdRequest( + Long userId +) { + +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserInfoRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserInfoRequest.java new file mode 100644 index 00000000..ab7c2969 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserInfoRequest.java @@ -0,0 +1,9 @@ +package sopt.org.hmh.domain.admin.dto.request; + +public record AdminUserInfoRequest( + Long userId, + String name, + Integer point +) { + +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/response/AdminTokenResponse.java b/src/main/java/sopt/org/hmh/domain/admin/dto/response/AdminTokenResponse.java new file mode 100644 index 00000000..8c6458a1 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/response/AdminTokenResponse.java @@ -0,0 +1,6 @@ +package sopt.org.hmh.domain.admin.dto.response; + +public record AdminTokenResponse( + String accessToken +) { +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/exception/AdminError.java b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminError.java new file mode 100644 index 00000000..61bdcb71 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminError.java @@ -0,0 +1,31 @@ +package sopt.org.hmh.domain.admin.exception; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import sopt.org.hmh.global.common.exception.base.ErrorBase; + +@AllArgsConstructor +public enum AdminError implements ErrorBase { + + // 401 UNAUTHORIZED + INVALID_ADMIN_AUTH_CODE(HttpStatus.UNAUTHORIZED, "관리자 인증 번호가 일치하지 않습니다."), + ; + + private final HttpStatus status; + private final String errorMessage; + + @Override + public int getHttpStatusCode() { + return this.status.value(); + } + + @Override + public HttpStatus getHttpStatus() { + return this.status; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/admin/exception/AdminException.java b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminException.java new file mode 100644 index 00000000..7c0ecfd8 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminException.java @@ -0,0 +1,10 @@ +package sopt.org.hmh.domain.admin.exception; + +import sopt.org.hmh.global.common.exception.base.ExceptionBase; + +public class AdminException extends ExceptionBase { + + public AdminException(AdminError errorBase) { + super(errorBase); + } +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/exception/AdminSuccess.java b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminSuccess.java new file mode 100644 index 00000000..fc9ba43c --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminSuccess.java @@ -0,0 +1,34 @@ +package sopt.org.hmh.domain.admin.exception; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import sopt.org.hmh.global.common.exception.base.SuccessBase; + +@AllArgsConstructor +public enum AdminSuccess implements SuccessBase { + + // 200 OK + ADMIN_LOGIN_SUCCESS(HttpStatus.OK, "관리자 로그인에 성공했습니다."), + + // 204 NO CONTENT + ADMIN_WITHDRAW_IMMEDIATELY_SUCCESS(HttpStatus.NO_CONTENT, "관리자 권한으로 유저 즉시 삭제에 성공했습니다."), + ; + + private final HttpStatus status; + private final String successMessage; + + @Override + public int getHttpStatusCode() { + return this.status.value(); + } + + @Override + public HttpStatus getHttpStatus() { + return this.status; + } + + @Override + public String getSuccessMessage() { + return this.successMessage; + } +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/service/AdminFacade.java b/src/main/java/sopt/org/hmh/domain/admin/service/AdminFacade.java new file mode 100644 index 00000000..6fbdc22d --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/service/AdminFacade.java @@ -0,0 +1,81 @@ +package sopt.org.hmh.domain.admin.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.hmh.domain.admin.dto.request.AdminDailyChallengeRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserInfoRequest; +import sopt.org.hmh.domain.admin.dto.response.AdminTokenResponse; +import sopt.org.hmh.domain.admin.exception.AdminError; +import sopt.org.hmh.domain.admin.exception.AdminException; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeError; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeException; +import sopt.org.hmh.domain.challenge.service.ChallengeService; +import sopt.org.hmh.domain.dailychallenge.domain.Status; +import sopt.org.hmh.domain.dailychallenge.service.DailyChallengeService; +import sopt.org.hmh.domain.user.domain.User; +import sopt.org.hmh.domain.user.service.UserService; +import sopt.org.hmh.global.auth.jwt.TokenService; + +@Service +@RequiredArgsConstructor +public class AdminFacade { + + @Value("${jwt.admin-auth-code}") + private String adminAuthCode; + + private final UserService userService; + private final TokenService tokenService; + private final ChallengeService challengeService; + private final DailyChallengeService dailyChallengeService; + + public AdminTokenResponse adminLogin(String authCode) { + validateAdminAuthCode(authCode); + return new AdminTokenResponse(tokenService.issueAdminToken()); + } + + private void validateAdminAuthCode(String authCode) { + if (!adminAuthCode.equals(authCode)) { + throw new AdminException(AdminError.INVALID_ADMIN_AUTH_CODE); + } + } + + @Transactional + public void withdrawImmediately(Long userId) { + userService.checkIsExistUserId(userId); + challengeService.deleteChallengeRelatedByUserId(userId); + userService.withdrawImmediately(userId); + } + + @Transactional + public void changeUserInfo(AdminUserInfoRequest request) { + User user = userService.findByIdOrThrowException(request.userId()); + if (Objects.nonNull(request.point())) { + user.changePoint(request.point()); + } + if (Objects.nonNull(request.name())) { + user.changeName(request.name()); + } + } + + @Transactional + public void changeDailyChallengeInfo(AdminDailyChallengeRequest request) { + Long currentChallengeId = userService.getCurrentChallengeIdByUserId(request.userId()); + List statuses = request.statuses(); + LocalDate challengeDate = request.startDate(); + + validateStatusesPeriod(currentChallengeId, statuses); + dailyChallengeService.changeInfoOfDailyChallenges(currentChallengeId, statuses, challengeDate); + } + + private void validateStatusesPeriod(Long challengeId, List statuses) { + Integer challengePeriod = challengeService.getChallengePeriod(challengeId); + if (challengePeriod != statuses.size()) { + throw new ChallengeException(ChallengeError.INVALID_PERIOD_NUMERIC); + } + } +} diff --git a/src/main/java/sopt/org/hmh/domain/app/domain/App.java b/src/main/java/sopt/org/hmh/domain/app/domain/App.java index 87212392..a4731b54 100644 --- a/src/main/java/sopt/org/hmh/domain/app/domain/App.java +++ b/src/main/java/sopt/org/hmh/domain/app/domain/App.java @@ -4,6 +4,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; +import jakarta.validation.constraints.NotNull; import lombok.Getter; @Getter @@ -14,7 +15,9 @@ public abstract class App { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotNull(message = "os는 null일 수 없습니다.") protected String os; + @NotNull(message = "appCode는 null일 수 없습니다.") protected String appCode; } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/app/domain/AppConstants.java b/src/main/java/sopt/org/hmh/domain/app/domain/AppConstants.java index 744ee28d..367d9328 100644 --- a/src/main/java/sopt/org/hmh/domain/app/domain/AppConstants.java +++ b/src/main/java/sopt/org/hmh/domain/app/domain/AppConstants.java @@ -6,5 +6,5 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public abstract class AppConstants { public static final Long MINIMUM_APP_TIME = 0L; - public static final Long MAXIMUM_APP_TIME = 21_600_000L; // 6시간 + public static final Long MAXIMUM_APP_TIME = 7_200_000L; // 2시간 } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/app/domain/ChallengeApp.java b/src/main/java/sopt/org/hmh/domain/app/domain/ChallengeApp.java index 4e10f79e..3534a27c 100644 --- a/src/main/java/sopt/org/hmh/domain/app/domain/ChallengeApp.java +++ b/src/main/java/sopt/org/hmh/domain/app/domain/ChallengeApp.java @@ -1,6 +1,7 @@ package sopt.org.hmh.domain.app.domain; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -16,6 +17,7 @@ public class ChallengeApp extends App { @JoinColumn(name = "challenge_id") private Challenge challenge; + @NotNull(message = "목표 시간은 null일 수 없습니다.") private Long goalTime; @Builder diff --git a/src/main/java/sopt/org/hmh/domain/app/domain/HistoryApp.java b/src/main/java/sopt/org/hmh/domain/app/domain/HistoryApp.java index 8425641d..addda272 100644 --- a/src/main/java/sopt/org/hmh/domain/app/domain/HistoryApp.java +++ b/src/main/java/sopt/org/hmh/domain/app/domain/HistoryApp.java @@ -1,6 +1,7 @@ package sopt.org.hmh.domain.app.domain; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -16,7 +17,10 @@ public class HistoryApp extends App { @JoinColumn(name = "daily_challenge_id") private DailyChallenge dailyChallenge; + @NotNull(message = "사용 시간은 null일 수 없습니다.") private Long usageTime; + + @NotNull(message = "목표 시간은 null일 수 없습니다.") private Long goalTime; @Builder diff --git a/src/main/java/sopt/org/hmh/domain/app/domain/exception/AppError.java b/src/main/java/sopt/org/hmh/domain/app/domain/exception/AppError.java index 5f417124..e422cb6e 100644 --- a/src/main/java/sopt/org/hmh/domain/app/domain/exception/AppError.java +++ b/src/main/java/sopt/org/hmh/domain/app/domain/exception/AppError.java @@ -9,9 +9,7 @@ public enum AppError implements ErrorBase { APP_NOT_FOUND(HttpStatus.NOT_FOUND, "앱을 찾을 수 없습니다."), APP_EXIST_ALREADY(HttpStatus.CONFLICT, "이미 추가된 앱입니다."), - INVALID_APP_CODE_NULL(HttpStatus.BAD_REQUEST, "앱 코드 값이 비어있습니다"), - INVALID_TIME_RANGE(HttpStatus.BAD_REQUEST, "앱 시간의 범위가 유효한지 확인해주세요"), - INVALID_TIME_NULL(HttpStatus.BAD_REQUEST, "앱 시간을 입력해주세요"), + INVALID_GOAL_TIME(HttpStatus.BAD_REQUEST, "앱 목표 시간이 유효하지 않습니다."), ; private final HttpStatus status; diff --git a/src/main/java/sopt/org/hmh/domain/app/dto/request/AppRemoveRequest.java b/src/main/java/sopt/org/hmh/domain/app/dto/request/AppRemoveRequest.java index f837fdf3..9167615d 100644 --- a/src/main/java/sopt/org/hmh/domain/app/dto/request/AppRemoveRequest.java +++ b/src/main/java/sopt/org/hmh/domain/app/dto/request/AppRemoveRequest.java @@ -1,6 +1,9 @@ package sopt.org.hmh.domain.app.dto.request; +import jakarta.validation.constraints.NotNull; + public record AppRemoveRequest( + @NotNull(message = "앱 코드는 null일 수 없습니다.") String appCode ) { } diff --git a/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppArrayRequest.java b/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppArrayRequest.java index 5c6f8b41..fab0b981 100644 --- a/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppArrayRequest.java +++ b/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppArrayRequest.java @@ -1,8 +1,9 @@ package sopt.org.hmh.domain.app.dto.request; +import jakarta.validation.Valid; import java.util.List; public record ChallengeAppArrayRequest( - List apps + List<@Valid ChallengeAppRequest> apps ) { } diff --git a/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppRequest.java b/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppRequest.java index f366e34f..20732ecd 100644 --- a/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppRequest.java +++ b/src/main/java/sopt/org/hmh/domain/app/dto/request/ChallengeAppRequest.java @@ -1,7 +1,30 @@ package sopt.org.hmh.domain.app.dto.request; +import jakarta.validation.constraints.NotNull; +import sopt.org.hmh.domain.app.domain.AppConstants; +import sopt.org.hmh.domain.app.domain.ChallengeApp; +import sopt.org.hmh.domain.app.domain.exception.AppError; +import sopt.org.hmh.domain.app.domain.exception.AppException; +import sopt.org.hmh.domain.challenge.domain.Challenge; + public record ChallengeAppRequest( + @NotNull(message = "앱 코드는 null일 수 없습니다.") String appCode, + @NotNull(message = "앱 시간은 null일 수 없습니다.") Long goalTime ) { + public ChallengeAppRequest { + if (goalTime > AppConstants.MAXIMUM_APP_TIME || goalTime < AppConstants.MINIMUM_APP_TIME) { + throw new AppException(AppError.INVALID_GOAL_TIME); + } + } + + public ChallengeApp toEntity(Challenge challenge , String os) { + return ChallengeApp.builder() + .challenge(challenge) + .appCode(appCode) + .goalTime(goalTime) + .os(os) + .build(); + } } diff --git a/src/main/java/sopt/org/hmh/domain/app/dto/request/HistoryAppRequest.java b/src/main/java/sopt/org/hmh/domain/app/dto/request/HistoryAppRequest.java index 4dde2153..524e154f 100644 --- a/src/main/java/sopt/org/hmh/domain/app/dto/request/HistoryAppRequest.java +++ b/src/main/java/sopt/org/hmh/domain/app/dto/request/HistoryAppRequest.java @@ -1,7 +1,11 @@ package sopt.org.hmh.domain.app.dto.request; +import jakarta.validation.constraints.NotNull; + public record HistoryAppRequest( + @NotNull(message = "앱 코드는 null일 수 없습니다.") String appCode, + @NotNull(message = "앱 사용시간은 null일 수 없습니다.") Long usageTime ) { } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/app/service/ChallengeAppService.java b/src/main/java/sopt/org/hmh/domain/app/service/ChallengeAppService.java new file mode 100644 index 00000000..366272f4 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/app/service/ChallengeAppService.java @@ -0,0 +1,40 @@ +package sopt.org.hmh.domain.app.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import sopt.org.hmh.domain.app.domain.ChallengeApp; +import sopt.org.hmh.domain.app.domain.exception.AppError; +import sopt.org.hmh.domain.app.domain.exception.AppException; +import sopt.org.hmh.domain.app.dto.request.ChallengeAppRequest; +import sopt.org.hmh.domain.app.repository.ChallengeAppRepository; +import sopt.org.hmh.domain.challenge.domain.Challenge; + +@Service +@RequiredArgsConstructor +public class ChallengeAppService { + + private final ChallengeAppRepository challengeAppRepository; + + public void removeApp(Challenge challenge, String appcode, String os) { + ChallengeApp appToRemove = + challengeAppRepository.findFirstByChallengeIdAndAppCodeAndOsOrElseThrow(challenge.getId(), appcode, os); + challengeAppRepository.delete(appToRemove); + } + + public void addApps(Challenge challenge, List requests, String os) { + challengeAppRepository.saveAll( + requests.stream().map( + request -> { + validateAppExist(challenge.getId(), request.appCode(), os); + return request.toEntity(challenge, os); + }).toList()); + } + + private void validateAppExist(Long challengeId, String appCode, String os) { + if (challengeAppRepository.existsByChallengeIdAndAppCodeAndOs(challengeId, appCode, os)) { + throw new AppException(AppError.APP_EXIST_ALREADY); + } + } + +} diff --git a/src/main/java/sopt/org/hmh/domain/app/service/AppService.java b/src/main/java/sopt/org/hmh/domain/app/service/HistoryAppService.java similarity index 85% rename from src/main/java/sopt/org/hmh/domain/app/service/AppService.java rename to src/main/java/sopt/org/hmh/domain/app/service/HistoryAppService.java index e6e24b93..c4a79d9a 100644 --- a/src/main/java/sopt/org/hmh/domain/app/service/AppService.java +++ b/src/main/java/sopt/org/hmh/domain/app/service/HistoryAppService.java @@ -3,7 +3,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import sopt.org.hmh.domain.app.domain.ChallengeApp; import sopt.org.hmh.domain.app.domain.HistoryApp; import sopt.org.hmh.domain.app.domain.exception.AppError; @@ -14,13 +13,13 @@ @Service @RequiredArgsConstructor -@Transactional -public class AppService { +public class HistoryAppService { private final HistoryAppRepository historyAppRepository; - public void addAppForHistory(List currentChallengeApps, List apps, - DailyChallenge dailyChallenge, String os) { + public void addHistoryApp( + List currentChallengeApps, List apps, + DailyChallenge dailyChallenge, String os) { historyAppRepository.saveAll(supplementAdditionalInfo(currentChallengeApps, apps, dailyChallenge, os)); } diff --git a/src/main/java/sopt/org/hmh/domain/auth/controller/AuthController.java b/src/main/java/sopt/org/hmh/domain/auth/controller/AuthController.java index f12e88a5..29eacd95 100644 --- a/src/main/java/sopt/org/hmh/domain/auth/controller/AuthController.java +++ b/src/main/java/sopt/org/hmh/domain/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package sopt.org.hmh.domain.auth.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -12,7 +13,7 @@ import sopt.org.hmh.domain.auth.exception.AuthSuccess; import sopt.org.hmh.domain.auth.dto.request.SocialPlatformRequest; import sopt.org.hmh.domain.auth.dto.request.SocialSignUpRequest; -import sopt.org.hmh.domain.auth.service.AuthService; +import sopt.org.hmh.domain.auth.service.AuthFacade; import sopt.org.hmh.global.auth.social.SocialAccessTokenResponse; import sopt.org.hmh.global.common.response.BaseResponse; @@ -21,7 +22,7 @@ @RequestMapping("/api/v1/user") public class AuthController implements AuthApi { - private final AuthService authService; + private final AuthFacade authFacade; @PostMapping("/login") @Override @@ -31,7 +32,7 @@ public ResponseEntity> orderLogin( ) { return ResponseEntity .status(AuthSuccess.LOGIN_SUCCESS.getHttpStatus()) - .body(BaseResponse.success(AuthSuccess.LOGIN_SUCCESS, authService.login(socialAccessToken, request))); + .body(BaseResponse.success(AuthSuccess.LOGIN_SUCCESS, authFacade.login(socialAccessToken, request))); } @PostMapping("/signup") @@ -39,11 +40,11 @@ public ResponseEntity> orderLogin( public ResponseEntity> orderSignup( @RequestHeader("Authorization") final String socialAccessToken, @RequestHeader("OS") final String os, - @RequestBody final SocialSignUpRequest request + @RequestBody @Valid final SocialSignUpRequest request ) { return ResponseEntity .status(AuthSuccess.SIGNUP_SUCCESS.getHttpStatus()) - .body(BaseResponse.success(AuthSuccess.SIGNUP_SUCCESS, authService.signup(socialAccessToken, request, os))); + .body(BaseResponse.success(AuthSuccess.SIGNUP_SUCCESS, authFacade.signup(socialAccessToken, request, os))); } @PostMapping("/reissue") @@ -53,7 +54,7 @@ public ResponseEntity> orderReissue( ) { return ResponseEntity .status(AuthSuccess.REISSUE_SUCCESS.getHttpStatus()) - .body(BaseResponse.success(AuthSuccess.REISSUE_SUCCESS, authService.reissueToken(refreshToken))); + .body(BaseResponse.success(AuthSuccess.REISSUE_SUCCESS, authFacade.reissueToken(refreshToken))); } @GetMapping("/social/token/kakao") @@ -62,6 +63,6 @@ public ResponseEntity> orderGetKakaoAcce ) { return ResponseEntity .status(AuthSuccess.GET_SOCIAL_ACCESS_TOKEN_SUCCESS.getHttpStatus()) - .body(BaseResponse.success(AuthSuccess.GET_SOCIAL_ACCESS_TOKEN_SUCCESS, authService.getSocialAccessTokenByAuthorizationCode(code))); + .body(BaseResponse.success(AuthSuccess.GET_SOCIAL_ACCESS_TOKEN_SUCCESS, authFacade.getSocialAccessTokenByAuthorizationCode(code))); } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/auth/dto/request/SocialSignUpRequest.java b/src/main/java/sopt/org/hmh/domain/auth/dto/request/SocialSignUpRequest.java index 9861dc6f..dc399a22 100644 --- a/src/main/java/sopt/org/hmh/domain/auth/dto/request/SocialSignUpRequest.java +++ b/src/main/java/sopt/org/hmh/domain/auth/dto/request/SocialSignUpRequest.java @@ -1,15 +1,43 @@ package sopt.org.hmh.domain.auth.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import sopt.org.hmh.domain.challenge.dto.request.ChallengeRequest; import sopt.org.hmh.domain.challenge.dto.request.ChallengeSignUpRequest; +import sopt.org.hmh.domain.user.domain.OnboardingInfo; +import sopt.org.hmh.domain.user.domain.OnboardingProblem; import sopt.org.hmh.global.auth.social.SocialPlatform; public record SocialSignUpRequest( + @NotNull(message = "소셜 플랫폼은 null일 수 없습니다.") SocialPlatform socialPlatform, String name, @JsonProperty(value = "onboarding") OnboardingRequest onboardingRequest, + @Valid @JsonProperty(value = "challenge") ChallengeSignUpRequest challengeSignUpRequest ) { + + public ChallengeRequest toChallengeRequest() { + return new ChallengeRequest(challengeSignUpRequest.period(), challengeSignUpRequest.goalTime()); + } + + public OnboardingInfo toOnboardingInfo(Long userId) { + return OnboardingInfo.builder() + .averageUseTime(onboardingRequest.averageUseTime()) + .userId(userId) + .build(); + } + + public List toProblemList(Long onboardingInfoId) { + return onboardingRequest.problemList().stream() + .map(problem -> OnboardingProblem.builder() + .onboardingInfoId(onboardingInfoId) + .problem(problem) + .build() + ).toList(); + } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/auth/dto/response/LoginResponse.java b/src/main/java/sopt/org/hmh/domain/auth/dto/response/LoginResponse.java index e3049c57..e5c16e3c 100644 --- a/src/main/java/sopt/org/hmh/domain/auth/dto/response/LoginResponse.java +++ b/src/main/java/sopt/org/hmh/domain/auth/dto/response/LoginResponse.java @@ -1,7 +1,6 @@ package sopt.org.hmh.domain.auth.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import sopt.org.hmh.domain.user.domain.User; import sopt.org.hmh.global.auth.jwt.TokenResponse; public record LoginResponse( @@ -9,12 +8,4 @@ public record LoginResponse( @JsonProperty(value = "token") TokenResponse tokenResponse ){ - - public static LoginResponse of(User loginUser, TokenResponse tokenResponse) { - return new LoginResponse( - loginUser.getId(), - tokenResponse - ); - } - } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/auth/exception/AuthError.java b/src/main/java/sopt/org/hmh/domain/auth/exception/AuthError.java index 2b80ff6b..4fe210e5 100644 --- a/src/main/java/sopt/org/hmh/domain/auth/exception/AuthError.java +++ b/src/main/java/sopt/org/hmh/domain/auth/exception/AuthError.java @@ -8,7 +8,6 @@ public enum AuthError implements ErrorBase { // 400 BAD REQUEST - INVALID_USER(HttpStatus.BAD_REQUEST, "Principle 객체가 없습니다."), DUPLICATE_USER(HttpStatus.BAD_REQUEST, "이미 회원가입된 유저입니다."), // 403 FORBIDDEN diff --git a/src/main/java/sopt/org/hmh/domain/auth/service/AuthService.java b/src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java similarity index 76% rename from src/main/java/sopt/org/hmh/domain/auth/service/AuthService.java rename to src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java index e1d0e9a3..5e314308 100644 --- a/src/main/java/sopt/org/hmh/domain/auth/service/AuthService.java +++ b/src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java @@ -3,9 +3,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import sopt.org.hmh.domain.app.service.ChallengeAppService; import sopt.org.hmh.domain.auth.dto.response.ReissueResponse; import sopt.org.hmh.domain.challenge.domain.Challenge; -import sopt.org.hmh.domain.challenge.service.ChallengeService; +import sopt.org.hmh.domain.challenge.service.ChallengeFacade; import sopt.org.hmh.domain.user.domain.User; import sopt.org.hmh.domain.auth.dto.request.SocialPlatformRequest; import sopt.org.hmh.domain.auth.dto.request.SocialSignUpRequest; @@ -21,22 +22,19 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) -public class AuthService { +public class AuthFacade { private final KakaoLoginService kakaoLoginService; private final AppleOAuthProvider appleOAuthProvider; - - private final ChallengeService challengeService; + private final ChallengeFacade challengeFacade; + private final ChallengeAppService challengeAppService; private final TokenService tokenService; private final UserService userService; - @Transactional + @Transactional(readOnly = true) public LoginResponse login(String socialAccessToken, SocialPlatformRequest request) { - SocialPlatform socialPlatform = request.socialPlatform(); - String socialId = getSocialIdBySocialAccessToken(socialPlatform, socialAccessToken); - + String socialId = this.getSocialIdBySocialAccessToken(socialPlatform, socialAccessToken); User loginUser = userService.getUserBySocialPlatformAndSocialId(socialPlatform, socialId); return performLogin(socialAccessToken, socialPlatform, loginUser); @@ -44,19 +42,16 @@ public LoginResponse login(String socialAccessToken, SocialPlatformRequest reque @Transactional public LoginResponse signup(String socialAccessToken, SocialSignUpRequest request, String os) { - SocialPlatform socialPlatform = request.socialPlatform(); - String socialId = getSocialIdBySocialAccessToken(socialPlatform, socialAccessToken); + String socialId = this.getSocialIdBySocialAccessToken(socialPlatform, socialAccessToken); userService.validateDuplicateUser(socialId, socialPlatform); - User user = userService.addUser(socialPlatform, socialId, request.name()); + Long userId = user.getId(); + userService.registerOnboardingInfo(request, userId); - Challenge challenge = challengeService.addChallenge(user.getId(), request.challengeSignUpRequest().period(), - request.challengeSignUpRequest().goalTime(), os); - challengeService.addApps(challenge, request.challengeSignUpRequest().apps(), os); - - userService.registerOnboardingInfo(request); + Challenge challenge = challengeFacade.addChallenge(userId, request.toChallengeRequest() , os); + challengeAppService.addApps(challenge, request.challengeSignUpRequest().apps(), os); return performLogin(socialAccessToken, socialPlatform, user); } @@ -73,16 +68,15 @@ public ReissueResponse reissueToken(String refreshToken) { return tokenService.reissueToken(refreshToken); } - private LoginResponse performLogin(String socialAccessToken, SocialPlatform socialPlatform, User loginUser) { if (socialPlatform == SocialPlatform.KAKAO) { kakaoLoginService.updateUserInfoByKakao(loginUser, socialAccessToken); } - return LoginResponse.of(loginUser, tokenService.issueToken(loginUser.getId())); + Long userId = loginUser.getId(); + return new LoginResponse(userId, tokenService.issueToken(userId.toString())); } public SocialAccessTokenResponse getSocialAccessTokenByAuthorizationCode(String code) { return kakaoLoginService.getKakaoAccessToken(code); } - } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java b/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java index 70be065e..3c503397 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java @@ -1,17 +1,17 @@ package sopt.org.hmh.domain.challenge.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import sopt.org.hmh.domain.app.domain.exception.AppSuccess; import sopt.org.hmh.domain.app.dto.request.ChallengeAppArrayRequest; import sopt.org.hmh.domain.app.dto.request.AppRemoveRequest; -import sopt.org.hmh.domain.challenge.domain.Challenge; import sopt.org.hmh.domain.challenge.domain.exception.ChallengeSuccess; import sopt.org.hmh.domain.challenge.dto.request.ChallengeRequest; import sopt.org.hmh.domain.challenge.dto.response.ChallengeResponse; import sopt.org.hmh.domain.challenge.dto.response.DailyChallengeResponse; -import sopt.org.hmh.domain.challenge.service.ChallengeService; +import sopt.org.hmh.domain.challenge.service.ChallengeFacade; import sopt.org.hmh.global.auth.UserId; import sopt.org.hmh.global.common.response.BaseResponse; import sopt.org.hmh.global.common.response.EmptyJsonResponse; @@ -21,14 +21,15 @@ @RequestMapping("/api/v1/challenge") public class ChallengeController implements ChallengeApi { - private final ChallengeService challengeService; + private final ChallengeFacade challengeFacade; @PostMapping @Override - public ResponseEntity> orderAddChallenge(@UserId final Long userId, - @RequestHeader("OS") final String os, - @RequestBody final ChallengeRequest request) { - challengeService.addChallenge(userId, request.period(), request.goalTime(), os); + public ResponseEntity> orderAddChallenge( + @UserId final Long userId, + @RequestHeader("OS") final String os, + @RequestBody @Valid final ChallengeRequest request) { + challengeFacade.addChallenge(userId, request, os); return ResponseEntity .status(ChallengeSuccess.ADD_CHALLENGE_SUCCESS.getHttpStatus()) @@ -37,31 +38,33 @@ public ResponseEntity> orderAddChallenge(@UserId final Long user @GetMapping @Override - public ResponseEntity> orderGetChallenge(@UserId final Long userId, - @RequestHeader("OS") final String os) { + public ResponseEntity> orderGetChallenge( + @UserId final Long userId, + @RequestHeader("OS") final String os) { return ResponseEntity .status(ChallengeSuccess.GET_CHALLENGE_SUCCESS.getHttpStatus()) .body(BaseResponse.success(ChallengeSuccess.GET_CHALLENGE_SUCCESS, - challengeService.getChallenge(userId))); + challengeFacade.getCurrentChallengeInfo(userId))); } @GetMapping("/home") @Override - public ResponseEntity> orderGetDailyChallenge(@UserId final Long userId, - @RequestHeader("OS") final String os) { + public ResponseEntity> orderGetDailyChallenge( + @UserId final Long userId, + @RequestHeader("OS") final String os) { return ResponseEntity .status(ChallengeSuccess.GET_DAILY_CHALLENGE_SUCCESS.getHttpStatus()) .body(BaseResponse.success(ChallengeSuccess.GET_DAILY_CHALLENGE_SUCCESS, - challengeService.getDailyChallenge(userId))); + challengeFacade.getDailyChallengeInfo(userId))); } @PostMapping("/app") @Override - public ResponseEntity> orderAddApps(@UserId final Long userId, - @RequestHeader("OS") final String os, - @RequestBody final ChallengeAppArrayRequest requests) { - Challenge challenge = challengeService.findCurrentChallengeByUserId(userId); - challengeService.addApps(challenge, requests.apps(), os); + public ResponseEntity> orderAddApps( + @UserId final Long userId, + @RequestHeader("OS") final String os, + @RequestBody @Valid final ChallengeAppArrayRequest requests) { + challengeFacade.addAppsToCurrentChallenge(userId, requests.apps(), os); return ResponseEntity .status(AppSuccess.ADD_APP_SUCCESS.getHttpStatus()) @@ -71,11 +74,11 @@ public ResponseEntity> orderAddApps(@UserId final Long userId, @DeleteMapping("/app") @Override - public ResponseEntity> orderRemoveApp(@UserId final Long userId, - @RequestHeader("OS") final String os, - @RequestBody final AppRemoveRequest request) { - Challenge challenge = challengeService.findCurrentChallengeByUserId(userId); - challengeService.removeApp(challenge, request, os); + public ResponseEntity> orderRemoveApp( + @UserId final Long userId, + @RequestHeader("OS") final String os, + @RequestBody @Valid final AppRemoveRequest request) { + challengeFacade.removeAppFromCurrentChallenge(userId, request.appCode(), os); return ResponseEntity .status(AppSuccess.REMOVE_APP_SUCCESS.getHttpStatus()) diff --git a/src/main/java/sopt/org/hmh/domain/challenge/domain/Challenge.java b/src/main/java/sopt/org/hmh/domain/challenge/domain/Challenge.java index a4ab2e9e..a33a1f61 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/domain/Challenge.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/domain/Challenge.java @@ -3,6 +3,7 @@ import static jakarta.persistence.GenerationType.*; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -22,15 +23,20 @@ public class Challenge extends BaseTimeEntity { @GeneratedValue(strategy = IDENTITY) private Long id; + @NotNull(message = "유저 아이디는 null일 수 없습니다.") private Long userId; + + @NotNull(message = "챌린지 기간은 null일 수 없습니다.") private Integer period; + + @NotNull(message = "목표 시간은 null일 수 없습니다.") private Long goalTime; @OneToMany(mappedBy = "challenge", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List apps = new ArrayList<>(); + private List apps; @OneToMany(mappedBy = "challenge", cascade = CascadeType.REMOVE, orphanRemoval = true) - private final List historyDailyChallenges = new ArrayList<>(); + private List historyDailyChallenges; @Builder private Challenge(Integer period, Long userId, Long goalTime, List apps) { diff --git a/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeConstants.java b/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeConstants.java index d12eb0c3..b9e3c9fe 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeConstants.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeConstants.java @@ -1,5 +1,6 @@ package sopt.org.hmh.domain.challenge.domain; +import java.util.List; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -9,5 +10,5 @@ public abstract class ChallengeConstants { public static final Long MAXIMUM_GOAL_TIME = 21_600_000L; // 6시간 public static final Integer USAGE_POINT = 100; public static final Integer EARNED_POINT = 20; - + public static final List AVAILABLE_CHALLENGE_PERIODS = List.of(7, 14, 20, 30); } diff --git a/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeDay.java b/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeDay.java deleted file mode 100644 index 0416b163..00000000 --- a/src/main/java/sopt/org/hmh/domain/challenge/domain/ChallengeDay.java +++ /dev/null @@ -1,18 +0,0 @@ -package sopt.org.hmh.domain.challenge.domain; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public enum ChallengeDay { - - DAYS7(7), - DAYS14(14), - DAYS20(20), - DAYS30(30), - ; - - private final int value; -} diff --git a/src/main/java/sopt/org/hmh/domain/challenge/domain/exception/ChallengeError.java b/src/main/java/sopt/org/hmh/domain/challenge/domain/exception/ChallengeError.java index b3e2563c..96aa7f16 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/domain/exception/ChallengeError.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/domain/exception/ChallengeError.java @@ -8,11 +8,8 @@ public enum ChallengeError implements ErrorBase { CHALLENGE_NOT_FOUND(HttpStatus.NOT_FOUND, "챌린지를 찾을 수 없습니다."), - INVALID_PERIOD_NULL(HttpStatus.BAD_REQUEST, "챌린지 기간은 null일 수 없습니다."), - INVALID_PERIOD_NUMERIC(HttpStatus.BAD_REQUEST, "유효한 숫자의 챌린지 기간을 입력해주세요."), - INVALID_GOAL_TIME_NULL(HttpStatus.BAD_REQUEST, "목표시간은 null일 수 없습니다."), - INVALID_GOAL_TIME_NUMERIC(HttpStatus.BAD_REQUEST, "유효한 숫자의 목표 시간을 입력해주세요."), - CHALLENGE_ALREADY_FAILED_TODAY(HttpStatus.BAD_REQUEST, "이미 실패 처리 된 챌린지입니다."), + INVALID_PERIOD_NUMERIC(HttpStatus.BAD_REQUEST, "유효한 챌린지 기간이 아닙니다."), + INVALID_GOAL_TIME(HttpStatus.BAD_REQUEST, "챌린지 목표 시간이 유효하지 않습니다."), ; private final HttpStatus status; diff --git a/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeDateRequest.java b/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeDateRequest.java new file mode 100644 index 00000000..b94f0828 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeDateRequest.java @@ -0,0 +1,10 @@ +package sopt.org.hmh.domain.challenge.dto.request; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +public record ChallengeDateRequest( + @NotNull(message = "챌린지 날짜는 null일 수 없습니다.") + LocalDate challengeDate +) { +} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeRequest.java b/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeRequest.java index 33ff3ba3..c2d60485 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeRequest.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeRequest.java @@ -1,7 +1,32 @@ package sopt.org.hmh.domain.challenge.dto.request; +import jakarta.validation.constraints.NotNull; +import sopt.org.hmh.domain.challenge.domain.Challenge; +import sopt.org.hmh.domain.challenge.domain.ChallengeConstants; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeError; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeException; + public record ChallengeRequest( + @NotNull(message = "챌린지 기간은 null일 수 없습니다.") Integer period, + @NotNull(message = "챌린지 목표시간은 null일 수 없습니다.") Long goalTime ) { -} \ No newline at end of file + + public ChallengeRequest { + if (!ChallengeConstants.AVAILABLE_CHALLENGE_PERIODS.contains(period)) { + throw new ChallengeException(ChallengeError.INVALID_PERIOD_NUMERIC); + } + if (goalTime > ChallengeConstants.MAXIMUM_GOAL_TIME || goalTime < ChallengeConstants.MINIMUM_GOAL_TIME) { + throw new ChallengeException(ChallengeError.INVALID_GOAL_TIME); + } + } + + public Challenge toEntity(Long userId) { + return Challenge.builder() + .userId(userId) + .period(period) + .goalTime(goalTime) + .build(); + } +} diff --git a/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeSignUpRequest.java b/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeSignUpRequest.java index 8491d7f5..527ca5d3 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeSignUpRequest.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/dto/request/ChallengeSignUpRequest.java @@ -1,12 +1,24 @@ package sopt.org.hmh.domain.challenge.dto.request; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import sopt.org.hmh.domain.app.dto.request.ChallengeAppRequest; import java.util.List; +import sopt.org.hmh.domain.challenge.domain.ChallengeConstants; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeError; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeException; public record ChallengeSignUpRequest( + @NotNull(message = "챌린지 기간은 null일 수 없습니다.") Integer period, + @NotNull(message = "챌린지 목표시간은 null일 수 없습니다.") Long goalTime, - List apps + List<@Valid ChallengeAppRequest> apps ) { + public ChallengeSignUpRequest { + if (goalTime > ChallengeConstants.MAXIMUM_GOAL_TIME || goalTime < ChallengeConstants.MINIMUM_GOAL_TIME) { + throw new ChallengeException(ChallengeError.INVALID_GOAL_TIME); + } + } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java b/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java index b426c20f..1126f585 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java @@ -10,4 +10,6 @@ public interface ChallengeRepository extends JpaRepository { Optional findById(Long id); void deleteByUserIdIn(List userId); + + void deleteByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeFacade.java b/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeFacade.java new file mode 100644 index 00000000..f49bbbc7 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeFacade.java @@ -0,0 +1,108 @@ +package sopt.org.hmh.domain.challenge.service; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.hmh.domain.app.dto.request.ChallengeAppRequest; +import sopt.org.hmh.domain.app.dto.response.ChallengeAppResponse; +import sopt.org.hmh.domain.app.service.ChallengeAppService; +import sopt.org.hmh.domain.challenge.domain.Challenge; +import sopt.org.hmh.domain.challenge.dto.request.ChallengeRequest; +import sopt.org.hmh.domain.challenge.dto.response.ChallengeResponse; +import sopt.org.hmh.domain.challenge.dto.response.DailyChallengeResponse; +import sopt.org.hmh.domain.dailychallenge.domain.DailyChallenge; +import sopt.org.hmh.domain.dailychallenge.service.DailyChallengeService; +import sopt.org.hmh.domain.user.domain.User; +import sopt.org.hmh.domain.user.service.UserService; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChallengeFacade { + + private final ChallengeService challengeService; + private final DailyChallengeService dailyChallengeService; + private final UserService userService; + private final ChallengeAppService challengeAppService; + + @Transactional + public Challenge addChallenge(Long userId, ChallengeRequest challengeRequest, String os) { + User user = userService.findByIdOrThrowException(userId); + Optional previousChallengeId = Optional.of(user.getCurrentChallengeId()); + + Challenge challenge = challengeService.save(challengeRequest.toEntity(userId)); + user.changeCurrentChallengeId(challenge.getId()); + + LocalDate startDate = challenge.getCreatedAt().toLocalDate(); + dailyChallengeService.addDailyChallenge(userId, startDate, challenge); + this.addAppsIfPreviousChallengeExist(os, previousChallengeId, challenge); + + return challenge; + } + + private void addAppsIfPreviousChallengeExist(String os, Optional previousChallengeId, Challenge challenge) { + if (previousChallengeId.isPresent()) { + Challenge previousChallenge = challengeService.findByIdOrElseThrow(previousChallengeId.get()); + List previousApps = previousChallenge.getApps().stream() + .map(app -> new ChallengeAppRequest(app.getAppCode(), app.getGoalTime())) + .toList(); + challengeAppService.addApps(challenge, previousApps, os); + } + } + + @Transactional(readOnly = true) + public ChallengeResponse getCurrentChallengeInfo(Long userId) { + Challenge challenge = this.findCurrentChallengeByUserId(userId); + + return ChallengeResponse.builder() + .period(challenge.getPeriod()) + .statuses(challenge.getHistoryDailyChallenges() + .stream() + .map(DailyChallenge::getStatus) + .toList()) + .todayIndex(this.calculateTodayIndex(challenge.getCreatedAt(), challenge.getPeriod())) + .startDate(challenge.getCreatedAt().toLocalDate().toString()) + .goalTime(challenge.getGoalTime()) + .apps(challenge.getApps().stream() + .map(app -> new ChallengeAppResponse(app.getAppCode(), app.getGoalTime())).toList()) + .build(); + } + + @Transactional(readOnly = true) + public DailyChallengeResponse getDailyChallengeInfo(Long userId) { + Challenge challenge = this.findCurrentChallengeByUserId(userId); + + return DailyChallengeResponse.builder() + .goalTime(challenge.getGoalTime()) + .apps(challenge.getApps().stream() + .map(app -> new ChallengeAppResponse(app.getAppCode(), app.getGoalTime())).toList()) + .build(); + } + + public Challenge findCurrentChallengeByUserId(Long userId) { + User user = userService.findByIdOrThrowException(userId); + return challengeService.findByIdOrElseThrow(user.getCurrentChallengeId()); + } + + private Integer calculateTodayIndex(LocalDateTime challengeCreateAt, int period) { + int daysBetween = (int) ChronoUnit.DAYS.between(challengeCreateAt.toLocalDate(), LocalDate.now()); + return (daysBetween >= period) ? -1 : daysBetween; + } + + @Transactional + public void addAppsToCurrentChallenge(Long userId, List requests, String os) { + Challenge challenge = this.findCurrentChallengeByUserId(userId); + challengeAppService.addApps(challenge, requests, os); + } + + @Transactional + public void removeAppFromCurrentChallenge(Long userId, String appCode, String os) { + Challenge challenge = this.findCurrentChallengeByUserId(userId); + challengeAppService.removeApp(challenge, appCode, os); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java b/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java index 628589bd..ccf0cf02 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java @@ -1,181 +1,26 @@ package sopt.org.hmh.domain.challenge.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import sopt.org.hmh.domain.app.domain.AppConstants; import sopt.org.hmh.domain.app.domain.ChallengeApp; -import sopt.org.hmh.domain.app.domain.exception.AppError; -import sopt.org.hmh.domain.app.domain.exception.AppException; -import sopt.org.hmh.domain.app.dto.request.AppRemoveRequest; -import sopt.org.hmh.domain.app.dto.request.ChallengeAppRequest; -import sopt.org.hmh.domain.app.dto.response.ChallengeAppResponse; -import sopt.org.hmh.domain.app.repository.ChallengeAppRepository; import sopt.org.hmh.domain.challenge.domain.Challenge; -import sopt.org.hmh.domain.challenge.domain.ChallengeConstants; -import sopt.org.hmh.domain.challenge.domain.ChallengeDay; import sopt.org.hmh.domain.challenge.domain.exception.ChallengeError; import sopt.org.hmh.domain.challenge.domain.exception.ChallengeException; -import sopt.org.hmh.domain.challenge.dto.response.ChallengeResponse; -import sopt.org.hmh.domain.challenge.dto.response.DailyChallengeResponse; import sopt.org.hmh.domain.challenge.repository.ChallengeRepository; -import sopt.org.hmh.domain.dailychallenge.domain.DailyChallenge; -import sopt.org.hmh.domain.dailychallenge.repository.DailyChallengeRepository; -import sopt.org.hmh.domain.user.domain.User; -import sopt.org.hmh.domain.user.service.UserService; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; @Service @RequiredArgsConstructor public class ChallengeService { private final ChallengeRepository challengeRepository; - private final ChallengeAppRepository challengeAppRepository; - private final DailyChallengeRepository dailyChallengeRepository; - private final UserService userService; - - @Transactional - public Challenge addChallenge(Long userId, Integer period, Long goalTime, String os) { - validateChallengePeriod(period); - validateChallengeGoalTime(goalTime); - - Challenge challenge = challengeRepository.save(Challenge.builder() - .userId(userId) - .period(period) - .goalTime(goalTime) - .build()); - - User user = userService.findByIdOrThrowException(userId); - Long previousChallengeId = user.getCurrentChallengeId(); - if (previousChallengeId != null) { - Challenge previousChallenge = findByIdOrElseThrow(previousChallengeId); - List previousApps = previousChallenge.getApps().stream() - .map(app -> new ChallengeAppRequest(app.getAppCode(), app.getGoalTime())) - .toList(); - addApps(challenge, previousApps, os); - } - - List dailyChallenges = new ArrayList<>(); - LocalDate startDate = challenge.getCreatedAt().toLocalDate(); - for (int dayCount = 0; dayCount < period; dayCount++) { - DailyChallenge dailyChallenge = DailyChallenge.builder() - .challengeDate(startDate.plusDays(dayCount)) - .challenge(challenge) - .userId(userId) - .goalTime(goalTime).build(); - dailyChallenges.add(dailyChallenge); - } - dailyChallengeRepository.saveAll(dailyChallenges); - - user.changeCurrentChallengeId(challenge.getId()); - - return challenge; - } - - public ChallengeResponse getChallenge(Long userId) { - Challenge challenge = findCurrentChallengeByUserId(userId); - Integer todayIndex = calculateTodayIndex(challenge.getCreatedAt(), challenge.getPeriod()); - - return ChallengeResponse.builder() - .period(challenge.getPeriod()) - .statuses(challenge.getHistoryDailyChallenges() - .stream() - .map(DailyChallenge::getStatus) - .toList()) - .todayIndex(todayIndex) - .startDate(challenge.getCreatedAt().toLocalDate().toString()) - .goalTime(challenge.getGoalTime()) - .apps(challenge.getApps().stream() - .map(app -> new ChallengeAppResponse(app.getAppCode(), app.getGoalTime())).toList()) - .build(); - } - - public DailyChallengeResponse getDailyChallenge(Long userId) { - Challenge challenge = findCurrentChallengeByUserId(userId); - - return DailyChallengeResponse.builder() - .goalTime(challenge.getGoalTime()) - .apps(challenge.getApps().stream() - .map(app -> new ChallengeAppResponse(app.getAppCode(), app.getGoalTime())).toList()) - .build(); - } - - @Transactional - public void removeApp(Challenge challenge, AppRemoveRequest request, String os) { - validateAppCode(request.appCode()); - ChallengeApp appToRemove = challengeAppRepository - .findFirstByChallengeIdAndAppCodeAndOsOrElseThrow(challenge.getId(), request.appCode(), os); - challengeAppRepository.delete(appToRemove); - } - @Transactional - public void addApps(Challenge challenge, List requests, String os) { - List appsToUpdate = requests.stream() - .map(request -> { - validateAppExist(challenge.getId(), request.appCode(), os); - validateAppCode(request.appCode()); - validateAppTime(request.goalTime()); - return ChallengeApp.builder() - .challenge(challenge) - .appCode(request.appCode()) - .goalTime(request.goalTime()) - .os(os) - .build(); - }).toList(); - challengeAppRepository.saveAll(appsToUpdate); - } - - @Transactional - public void deleteChallengeRelatedByUserId(List expiredUserIdList) { + public void deleteChallengeRelatedByUserIds(List expiredUserIdList) { challengeRepository.deleteByUserIdIn(expiredUserIdList); } - private Integer calculateTodayIndex(LocalDateTime challengeCreateAt, int period) { - int daysBetween = (int) ChronoUnit.DAYS.between(challengeCreateAt.toLocalDate(), LocalDate.now()); - return (daysBetween >= period) ? -1 : daysBetween; - } - - private void validateChallengePeriod(Integer period) { - if (period == null) { - throw new ChallengeException(ChallengeError.INVALID_PERIOD_NULL); - } - if (period != ChallengeDay.DAYS7.getValue() && period != ChallengeDay.DAYS14.getValue() && period != ChallengeDay.DAYS20.getValue() && period != ChallengeDay.DAYS30.getValue()) { - throw new ChallengeException(ChallengeError.INVALID_PERIOD_NUMERIC); - } - } - - private void validateChallengeGoalTime(Long goalTime) { - if (goalTime == null) { - throw new ChallengeException(ChallengeError.INVALID_GOAL_TIME_NULL); - } - if (goalTime < ChallengeConstants.MINIMUM_GOAL_TIME || goalTime > ChallengeConstants.MAXIMUM_GOAL_TIME) { - throw new ChallengeException(ChallengeError.INVALID_GOAL_TIME_NUMERIC); - } - } - - private void validateAppExist(Long challengeId, String appCode, String os) { - if (challengeAppRepository.existsByChallengeIdAndAppCodeAndOs(challengeId, appCode, os)) { - throw new AppException(AppError.APP_EXIST_ALREADY); - } - } - - private void validateAppCode(String appCode) { - if (appCode.isEmpty()) { - throw new AppException(AppError.INVALID_APP_CODE_NULL); - } - } - - private void validateAppTime(Long appTime) { - if (appTime == null) { - throw new AppException(AppError.INVALID_TIME_NULL); - } - if (appTime > AppConstants.MAXIMUM_APP_TIME || appTime < AppConstants.MINIMUM_APP_TIME) - throw new AppException(AppError.INVALID_TIME_RANGE); + public void deleteChallengeRelatedByUserId(Long userId) { + challengeRepository.deleteByUserId(userId); } public Challenge findByIdOrElseThrow(Long challengeId) { @@ -183,12 +28,15 @@ public Challenge findByIdOrElseThrow(Long challengeId) { () -> new ChallengeException(ChallengeError.CHALLENGE_NOT_FOUND)); } - public Challenge findCurrentChallengeByUserId(Long userId) { - User user = userService.findByIdOrThrowException(userId); - return findByIdOrElseThrow(user.getCurrentChallengeId()); - } - public List getCurrentChallengeAppByChallengeId(Long challengeId) { return this.findByIdOrElseThrow(challengeId).getApps(); } -} \ No newline at end of file + + public Challenge save(Challenge challenge) { + return challengeRepository.save(challenge); + } + + public Integer getChallengePeriod(Long challengeId) { + return findByIdOrElseThrow(challengeId).getPeriod(); + } +} diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/controller/DailyChallengeController.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/controller/DailyChallengeController.java index 6f3c37a1..63c045c5 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/controller/DailyChallengeController.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/controller/DailyChallengeController.java @@ -1,5 +1,6 @@ package sopt.org.hmh.domain.dailychallenge.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -7,7 +8,6 @@ import sopt.org.hmh.domain.dailychallenge.dto.request.FinishedDailyChallengeListRequest; import sopt.org.hmh.domain.dailychallenge.dto.request.FinishedDailyChallengeStatusListRequest; import sopt.org.hmh.domain.dailychallenge.service.DailyChallengeFacade; -import sopt.org.hmh.domain.dailychallenge.service.DailyChallengeService; import sopt.org.hmh.global.auth.UserId; import sopt.org.hmh.global.common.response.BaseResponse; import sopt.org.hmh.global.common.response.EmptyJsonResponse; @@ -18,14 +18,13 @@ public class DailyChallengeController implements DailyChallengeApi { private final DailyChallengeFacade dailyChallengeFacade; - private final DailyChallengeService dailyChallengeService; @Override @PostMapping("/finish") public ResponseEntity> orderAddHistoryDailyChallenge( @UserId final Long userId, @RequestHeader("OS") final String os, - @RequestBody final FinishedDailyChallengeListRequest request + @RequestBody @Valid final FinishedDailyChallengeListRequest request ) { dailyChallengeFacade.addFinishedDailyChallengeHistory(userId, request, os); return ResponseEntity @@ -40,7 +39,7 @@ public ResponseEntity> orderChangeStatusDailyCha @RequestHeader("OS") final String os, @RequestBody final FinishedDailyChallengeStatusListRequest request ) { - dailyChallengeService.changeDailyChallengeStatusByIsSuccess(userId, request); + dailyChallengeFacade.changeDailyChallengeStatusByIsSuccess(userId, request); return ResponseEntity .status(DailyChallengeSuccess.SEND_FINISHED_DAILY_CHALLENGE_SUCCESS.getHttpStatus()) .body(BaseResponse.success(DailyChallengeSuccess.SEND_FINISHED_DAILY_CHALLENGE_SUCCESS, new EmptyJsonResponse())); diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java index 440c92ac..2d568237 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java @@ -3,12 +3,12 @@ import static jakarta.persistence.GenerationType.*; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.util.Assert; import sopt.org.hmh.domain.app.domain.HistoryApp; import sopt.org.hmh.global.common.domain.BaseTimeEntity; import sopt.org.hmh.domain.challenge.domain.Challenge; @@ -25,27 +25,27 @@ public class DailyChallenge extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "challenge_id") + @NotNull(message = "챌린지는 null일 수 없습니다.") private Challenge challenge; @OneToMany(mappedBy = "dailyChallenge", cascade = CascadeType.REMOVE, orphanRemoval = true) private List apps; @Enumerated(EnumType.STRING) + @NotNull(message = "status값은 null일 수 없습니다.") private Status status; + @NotNull(message = "유저 아이디는 null일 수 없습니다.") private Long userId; + @NotNull(message = "목표 시간은 null일 수 없습니다.") private Long goalTime; + @NotNull(message = "챌린지 날짜는 null일 수 없습니다.") private LocalDate challengeDate; @Builder DailyChallenge(Challenge challenge, Long userId, Long goalTime, LocalDate challengeDate) { - Assert.notNull(challenge, "Challenge must not be null"); - Assert.notNull(userId, "UserId must not be null"); - Assert.notNull(goalTime, "GoalTime must not be null"); - Assert.notNull(challengeDate, "ChallengeDate must not be null"); - this.challenge = challenge; this.userId = userId; this.goalTime = goalTime; @@ -53,6 +53,10 @@ public class DailyChallenge extends BaseTimeEntity { this.status = Status.NONE; } + public void changeChallengeDate(LocalDate challengeDate) { + this.challengeDate = challengeDate; + } + public void changeStatus(Status status) { this.status = status; } diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/exception/DailyChallengeError.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/exception/DailyChallengeError.java index e4bea843..60c5b7aa 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/exception/DailyChallengeError.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/exception/DailyChallengeError.java @@ -8,7 +8,6 @@ public enum DailyChallengeError implements ErrorBase { DAILY_CHALLENGE_NOT_FOUND(HttpStatus.NOT_FOUND, "일별 챌린지를 찾을 수 없습니다."), - DAILY_CHALLENGE_YESTERDAY_NOT_FOUND(HttpStatus.NOT_FOUND, "어제의 일별 챌린지를 찾을 수 없습니다."), DAILY_CHALLENGE_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "이미 처리된 일별 챌린지입니다.") ; diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeListRequest.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeListRequest.java index 5e5c9c87..cae0e1da 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeListRequest.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeListRequest.java @@ -1,9 +1,10 @@ package sopt.org.hmh.domain.dailychallenge.dto.request; +import jakarta.validation.Valid; import java.util.List; public record FinishedDailyChallengeListRequest( - List finishedDailyChallenges + List<@Valid FinishedDailyChallengeRequest> finishedDailyChallenges ) { } diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeRequest.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeRequest.java index 19d1b777..540bc927 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeRequest.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/dto/request/FinishedDailyChallengeRequest.java @@ -1,11 +1,14 @@ package sopt.org.hmh.domain.dailychallenge.dto.request; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; import sopt.org.hmh.domain.app.dto.request.HistoryAppRequest; public record FinishedDailyChallengeRequest( + @NotNull(message = "챌린지 날짜는 null일 수 없습니다.") LocalDate challengeDate, - List apps + List<@Valid HistoryAppRequest> apps ) { } diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java index e08921fd..ca5bd335 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java @@ -1,10 +1,14 @@ package sopt.org.hmh.domain.dailychallenge.repository; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import sopt.org.hmh.domain.dailychallenge.domain.DailyChallenge; public interface DailyChallengeRepository extends JpaRepository { + Optional findByChallengeDateAndUserId(LocalDate challengeDate, Long userId); + + List findAllByChallengeId(Long challengeId); } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java index be3ba3f1..b4ba716e 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java @@ -5,29 +5,50 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sopt.org.hmh.domain.app.domain.ChallengeApp; -import sopt.org.hmh.domain.app.service.AppService; +import sopt.org.hmh.domain.app.service.HistoryAppService; import sopt.org.hmh.domain.challenge.service.ChallengeService; import sopt.org.hmh.domain.dailychallenge.domain.DailyChallenge; +import sopt.org.hmh.domain.dailychallenge.domain.Status; import sopt.org.hmh.domain.dailychallenge.dto.request.FinishedDailyChallengeListRequest; +import sopt.org.hmh.domain.dailychallenge.dto.request.FinishedDailyChallengeStatusListRequest; import sopt.org.hmh.domain.user.service.UserService; @Service @RequiredArgsConstructor -@Transactional public class DailyChallengeFacade { private final DailyChallengeService dailyChallengeService; - private final AppService appService; + private final HistoryAppService historyAppService; private final ChallengeService challengeService; private final UserService userService; + @Transactional public void addFinishedDailyChallengeHistory(Long userId, FinishedDailyChallengeListRequest requests, String os) { + Long currentChallengeId = userService.getCurrentChallengeIdByUserId(userId); + List currentChallengeApps = + challengeService.getCurrentChallengeAppByChallengeId(currentChallengeId); + requests.finishedDailyChallenges().forEach(request -> { - DailyChallenge dailyChallenge = dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); + DailyChallenge dailyChallenge = + dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); dailyChallengeService.changeStatusByCurrentStatus(dailyChallenge); - Long currentChallengeId = userService.getCurrentChallengeIdByUserId(userId); - List currentChallengeApps = challengeService.getCurrentChallengeAppByChallengeId(currentChallengeId); - appService.addAppForHistory(currentChallengeApps, request.apps(), dailyChallenge, os); + historyAppService.addHistoryApp(currentChallengeApps, request.apps(), dailyChallenge, os); + }); + } + + @Transactional + public void changeDailyChallengeStatusByIsSuccess(Long userId, FinishedDailyChallengeStatusListRequest requests) { + requests.finishedDailyChallenges().forEach(request -> { + DailyChallenge dailyChallenge = + dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); + if (request.isSuccess()) { + dailyChallengeService.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.NONE)); + dailyChallenge.changeStatus(Status.UNEARNED); + } else { + dailyChallengeService.validateDailyChallengeStatus( + dailyChallenge.getStatus(), List.of(Status.NONE, Status.FAILURE)); + dailyChallenge.changeStatus(Status.FAILURE); + } }); } } diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java index bf8a48ca..c5ee346e 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java @@ -2,19 +2,18 @@ import java.time.LocalDate; import java.util.List; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import sopt.org.hmh.domain.challenge.domain.Challenge; import sopt.org.hmh.domain.dailychallenge.domain.DailyChallenge; import sopt.org.hmh.domain.dailychallenge.domain.Status; import sopt.org.hmh.domain.dailychallenge.domain.exception.DailyChallengeError; import sopt.org.hmh.domain.dailychallenge.domain.exception.DailyChallengeException; -import sopt.org.hmh.domain.dailychallenge.dto.request.FinishedDailyChallengeStatusListRequest; import sopt.org.hmh.domain.dailychallenge.repository.DailyChallengeRepository; @Service @RequiredArgsConstructor -@Transactional public class DailyChallengeService { private final DailyChallengeRepository dailyChallengeRepository; @@ -25,15 +24,7 @@ public DailyChallenge findByChallengeDateAndUserIdOrThrowException(LocalDate cha } public void validateDailyChallengeStatus(Status dailyChallengeStatus, List expectedStatuses) { - boolean isAlreadyProcessed = true; - for (Status expected : expectedStatuses) { - if (dailyChallengeStatus == expected) { - isAlreadyProcessed = false; - break; - } - } - - if (isAlreadyProcessed) { + if (!expectedStatuses.contains(dailyChallengeStatus)) { throw new DailyChallengeException(DailyChallengeError.DAILY_CHALLENGE_ALREADY_PROCESSED); } } @@ -53,16 +44,35 @@ private void handleAlreadyProcessedDailyChallenge(DailyChallenge dailyChallenge) throw new DailyChallengeException(DailyChallengeError.DAILY_CHALLENGE_ALREADY_PROCESSED); } - public void changeDailyChallengeStatusByIsSuccess(Long userId, FinishedDailyChallengeStatusListRequest requests) { - requests.finishedDailyChallenges().forEach(request -> { - DailyChallenge dailyChallenge = this.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); - if (request.isSuccess()) { - this.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.NONE)); - dailyChallenge.changeStatus(Status.UNEARNED); - } else { - this.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.NONE, Status.FAILURE)); - dailyChallenge.changeStatus(Status.FAILURE); - } - }); + public void addDailyChallenge(Long userId, LocalDate startDate, Challenge challenge) { + dailyChallengeRepository.saveAll(IntStream.range(0, challenge.getPeriod()) + .mapToObj(i -> DailyChallenge.builder() + .challengeDate(startDate.plusDays(i)) + .challenge(challenge) + .userId(userId) + .goalTime(challenge.getGoalTime()).build()) + .toList()); + } + + public List getDailyChallengesByChallengeId(Long challengeId) { + return dailyChallengeRepository.findAllByChallengeId(challengeId); + } + + public void changeInfoOfDailyChallenges(Long challengeId, List statuses, LocalDate challengeDate) { + List dailyChallenges = getDailyChallengesByChallengeId(challengeId); + changeStatusOfDailyChallenges(dailyChallenges, statuses); + changeChallengeDateOfDailyChallenges(dailyChallenges, challengeDate); + } + + private void changeStatusOfDailyChallenges(List dailyChallenges, List statuses) { + for (int i = 0; i < dailyChallenges.size(); i++) { + dailyChallenges.get(i).changeStatus(statuses.get(i)); + } + } + + private void changeChallengeDateOfDailyChallenges(List dailyChallenges, LocalDate challengeDate) { + for (int i = 0; i < dailyChallenges.size(); i++) { + dailyChallenges.get(i).changeChallengeDate(challengeDate.plusDays(i)); + } } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/point/controller/PointApi.java b/src/main/java/sopt/org/hmh/domain/point/controller/PointApi.java index 97d5eea4..45bbb357 100644 --- a/src/main/java/sopt/org/hmh/domain/point/controller/PointApi.java +++ b/src/main/java/sopt/org/hmh/domain/point/controller/PointApi.java @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import sopt.org.hmh.domain.point.dto.request.ChallengeDateRequest; +import sopt.org.hmh.domain.challenge.dto.request.ChallengeDateRequest; import sopt.org.hmh.domain.point.dto.response.ChallengePointStatusListResponse; import sopt.org.hmh.domain.point.dto.response.EarnPointResponse; import sopt.org.hmh.domain.point.dto.response.EarnedPointResponse; diff --git a/src/main/java/sopt/org/hmh/domain/point/controller/PointController.java b/src/main/java/sopt/org/hmh/domain/point/controller/PointController.java index 5ed744ea..7e7b9558 100644 --- a/src/main/java/sopt/org/hmh/domain/point/controller/PointController.java +++ b/src/main/java/sopt/org/hmh/domain/point/controller/PointController.java @@ -1,5 +1,6 @@ package sopt.org.hmh.domain.point.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -7,8 +8,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import sopt.org.hmh.domain.challenge.dto.request.ChallengeDateRequest; import sopt.org.hmh.domain.challenge.domain.ChallengeConstants; -import sopt.org.hmh.domain.point.dto.request.ChallengeDateRequest; import sopt.org.hmh.domain.point.dto.response.*; import sopt.org.hmh.domain.point.exception.PointSuccess; import sopt.org.hmh.domain.point.service.PointFacade; @@ -38,7 +39,7 @@ public ResponseEntity> orderGetCh @PatchMapping("/use") public ResponseEntity> orderUsagePointAndChallengeFailed( @UserId final Long userId, - @RequestBody final ChallengeDateRequest challengeDateRequest + @RequestBody @Valid final ChallengeDateRequest challengeDateRequest ) { return ResponseEntity .status(PointSuccess.POINT_USAGE_SUCCESS.getHttpStatus()) diff --git a/src/main/java/sopt/org/hmh/domain/point/dto/request/ChallengeDateRequest.java b/src/main/java/sopt/org/hmh/domain/point/dto/request/ChallengeDateRequest.java deleted file mode 100644 index 931db058..00000000 --- a/src/main/java/sopt/org/hmh/domain/point/dto/request/ChallengeDateRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package sopt.org.hmh.domain.point.dto.request; - -import java.time.LocalDate; - -public record ChallengeDateRequest( - LocalDate challengeDate -) { -} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/point/service/PointFacade.java b/src/main/java/sopt/org/hmh/domain/point/service/PointFacade.java index 606db230..295caa80 100644 --- a/src/main/java/sopt/org/hmh/domain/point/service/PointFacade.java +++ b/src/main/java/sopt/org/hmh/domain/point/service/PointFacade.java @@ -18,15 +18,16 @@ @Service @RequiredArgsConstructor -@Transactional public class PointFacade { private final UserService userService; private final DailyChallengeService dailyChallengeService; private final ChallengeService challengeService; + @Transactional public UsePointResponse usePointAndChallengeFailed(Long userId, LocalDate challengeDate) { - DailyChallenge dailyChallenge = dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(challengeDate, userId); + DailyChallenge dailyChallenge = + dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(challengeDate, userId); User user = userService.findByIdOrThrowException(userId); dailyChallengeService.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.NONE)); @@ -38,20 +39,22 @@ public UsePointResponse usePointAndChallengeFailed(Long userId, LocalDate challe ); } + @Transactional public EarnPointResponse earnPointAndChallengeEarned(Long userId, LocalDate challengeDate) { - DailyChallenge dailyChallenge = dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(challengeDate, userId); + DailyChallenge dailyChallenge = + dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(challengeDate, userId); User user = userService.findByIdOrThrowException(userId); dailyChallengeService.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.UNEARNED)); dailyChallenge.changeStatus(Status.EARNED); - return new EarnPointResponse( - user.increasePoint(ChallengeConstants.EARNED_POINT) - ); + return new EarnPointResponse(user.increasePoint(ChallengeConstants.EARNED_POINT)); } + @Transactional(readOnly = true) public ChallengePointStatusListResponse getChallengePointStatusList(Long userId) { - Challenge challenge = challengeService.findCurrentChallengeByUserId(userId); + User user = userService.findByIdOrThrowException(userId); + Challenge challenge = challengeService.findByIdOrElseThrow(user.getCurrentChallengeId()); List challengePointStatusResponseList = challenge.getHistoryDailyChallenges().stream() .map(dailyChallenge -> new ChallengePointStatusResponse( @@ -59,7 +62,7 @@ public ChallengePointStatusListResponse getChallengePointStatusList(Long userId) dailyChallenge.getStatus())).toList(); return new ChallengePointStatusListResponse( - userService.getUserInfo(userId).point(), + user.getPoint(), challenge.getPeriod(), challengePointStatusResponseList ); diff --git a/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingInfo.java b/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingInfo.java index 08db01bb..4fbb39a9 100644 --- a/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingInfo.java +++ b/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingInfo.java @@ -4,6 +4,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -18,7 +19,10 @@ public class OnboardingInfo { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotNull(message = "평균 사용 시간은 null일 수 없습니다.") private String averageUseTime; + + @NotNull(message = "유저 아이디는 null일 수 없습니다.") private Long userId; @Builder diff --git a/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingProblem.java b/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingProblem.java index 5220bd5b..81844d1c 100644 --- a/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingProblem.java +++ b/src/main/java/sopt/org/hmh/domain/user/domain/OnboardingProblem.java @@ -5,6 +5,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -20,7 +21,9 @@ public class OnboardingProblem { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotNull(message = "온보딩 정보 아이디는 null일 수 없습니다.") private Long onboardingInfoId; + @NotNull(message = "문제 항목은 null일 수 없습니다.") private String problem; @Builder diff --git a/src/main/java/sopt/org/hmh/domain/user/domain/User.java b/src/main/java/sopt/org/hmh/domain/user/domain/User.java index d67594a8..fb831734 100644 --- a/src/main/java/sopt/org/hmh/domain/user/domain/User.java +++ b/src/main/java/sopt/org/hmh/domain/user/domain/User.java @@ -11,6 +11,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalDateTime; import lombok.AccessLevel; @@ -31,21 +32,22 @@ public class User extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; + private Long currentChallengeId; @Enumerated(EnumType.STRING) + @NotNull(message = "소셜 플랫폼은 null일 수 없습니다.") private SocialPlatform socialPlatform; @Column(unique = true) private String socialId; @Min(value = 0) + @NotNull(message = "포인트는 기본 값이 설정되어야 합니다.") private Integer point; - @Column(columnDefinition = "TEXT") - private String profileImageUrl; - private LocalDate recentLockDate; private boolean isDeleted = false; @@ -59,9 +61,8 @@ public User(SocialPlatform socialPlatform, String socialId, String name) { this.point = UserConstants.INITIAL_POINT; } - public void updateSocialInfo(String nickname, String profileImageUrl) { + public void updateNickname(String nickname) { this.name = nickname; - this.profileImageUrl = profileImageUrl; } public void softDelete() { @@ -88,6 +89,14 @@ public Integer increasePoint(Integer earnedPoint) { return this.point; } + public void changePoint(Integer point) { + this.point = point; + } + + public void changeName(String name) { + this.name = name; + } + public void changeCurrentChallengeId(Long currentChallengeId) { this.currentChallengeId = currentChallengeId; } diff --git a/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java b/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java index 578445b3..e3f096f8 100644 --- a/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java +++ b/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java @@ -11,13 +11,13 @@ @Component @RequiredArgsConstructor -@Transactional public class ExpiredUserDeleteScheduler { private final UserRepository userRepository; private final ChallengeService challengeService; @Scheduled(cron = "0 0 4 * * ?") + @Transactional public void deleteExpiredUser() { deleteExpiredUser(LocalDateTime.now()); } @@ -25,6 +25,6 @@ public void deleteExpiredUser() { public void deleteExpiredUser(LocalDateTime currentDate) { List expiredUserList = userRepository.findIdByDeletedAtBeforeAndIsDeletedIsTrue(currentDate); userRepository.deleteAllById(expiredUserList); - challengeService.deleteChallengeRelatedByUserId(expiredUserList); + challengeService.deleteChallengeRelatedByUserIds(expiredUserList); } } diff --git a/src/main/java/sopt/org/hmh/domain/user/service/UserService.java b/src/main/java/sopt/org/hmh/domain/user/service/UserService.java index 54466c4d..397c5252 100644 --- a/src/main/java/sopt/org/hmh/domain/user/service/UserService.java +++ b/src/main/java/sopt/org/hmh/domain/user/service/UserService.java @@ -1,7 +1,6 @@ package sopt.org.hmh.domain.user.service; import java.time.LocalDate; -import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -13,8 +12,6 @@ import sopt.org.hmh.domain.auth.exception.AuthException; import sopt.org.hmh.domain.auth.repository.OnboardingInfoRepository; import sopt.org.hmh.domain.auth.repository.ProblemRepository; -import sopt.org.hmh.domain.user.domain.OnboardingInfo; -import sopt.org.hmh.domain.user.domain.OnboardingProblem; import sopt.org.hmh.domain.user.domain.User; import sopt.org.hmh.domain.user.domain.UserConstants; import sopt.org.hmh.domain.user.domain.exception.UserError; @@ -26,19 +23,18 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) public class UserService { private final UserRepository userRepository; private final OnboardingInfoRepository onboardingInfoRepository; private final ProblemRepository problemRepository; - @Transactional public void withdraw(Long userId) { this.findByIdOrThrowException(userId).softDelete(); } + @Transactional(readOnly = true) public UserInfoResponse getUserInfo(Long userId) { return UserInfoResponse.of(this.findByIdOrThrowException(userId)); } @@ -74,21 +70,12 @@ private String validateName(String name) { return name; } - public void registerOnboardingInfo(SocialSignUpRequest request) { - OnboardingInfo onboardingInfo = OnboardingInfo.builder() - .averageUseTime(request.onboardingRequest().averageUseTime()) - .build(); - Long onboardingInfoId = onboardingInfoRepository.save(onboardingInfo).getId(); - - List problemList = request.onboardingRequest().problemList().stream() - .map(problem -> OnboardingProblem.builder() - .onboardingInfoId(onboardingInfoId) - .problem(problem).build()) - .toList(); - problemRepository.saveAll(problemList); + public void registerOnboardingInfo(SocialSignUpRequest request, Long userId) { + Long onboardingInfoId = onboardingInfoRepository.save(request.toOnboardingInfo(userId)).getId(); + problemRepository.saveAll(request.toProblemList(onboardingInfoId)); } - public User findBySocialPlatformAndSocialIdOrThrowException(SocialPlatform socialPlatform, String socialId) { + private User findBySocialPlatformAndSocialIdOrThrowException(SocialPlatform socialPlatform, String socialId) { return userRepository.findBySocialPlatformAndSocialId(socialPlatform, socialId).orElseThrow( () -> new AuthException(AuthError.NOT_SIGNUP_USER)); } @@ -98,23 +85,33 @@ public User findByIdOrThrowException(Long userId) { () -> new UserException(UserError.NOT_FOUND_USER)); } + private boolean isExistUserId(Long userId) { + return userRepository.existsById(userId); + } + + public void checkIsExistUserId(Long userId) { + if (!isExistUserId(userId)) { + throw new UserException(UserError.NOT_FOUND_USER); + } + } + public Long getCurrentChallengeIdByUserId(Long userId) { return Optional.ofNullable(this.findByIdOrThrowException(userId).getCurrentChallengeId()) .orElseThrow(() -> new UserException(UserError.NOT_FOUND_CURRENT_CHALLENGE_ID)); } @Transactional - public void changeRecentLockDate(Long userId, LocalDate localDate) { - this.findByIdOrThrowException(userId).changeRecentLockDate(localDate); + public void changeRecentLockDate(Long userId, LocalDate lockDate) { + this.findByIdOrThrowException(userId).changeRecentLockDate(lockDate); } + @Transactional(readOnly = true) public IsLockTodayResponse checkIsTodayLock(Long userId, LocalDate lockCheckDate) { LocalDate userRecentLockDate = this.findByIdOrThrowException(userId).getRecentLockDate(); + return new IsLockTodayResponse(lockCheckDate.equals(userRecentLockDate)); + } - if (userRecentLockDate == null) { - return new IsLockTodayResponse(false); - } - - return new IsLockTodayResponse(userRecentLockDate.equals(lockCheckDate)); + public void withdrawImmediately(Long userId) { + userRepository.deleteById(userId); } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java b/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java index 87c5dd1e..0318b423 100644 --- a/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java +++ b/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java @@ -21,9 +21,11 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - return SecurityContextHolder.getContext() + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + return Long.parseLong(SecurityContextHolder.getContext() .getAuthentication() - .getPrincipal(); + .getPrincipal() + .toString()); } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java index 792d2ee9..4e5ee2b8 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java @@ -8,4 +8,5 @@ public abstract class JwtConstants { public static final String AUTHORIZATION = "Authorization"; public static final String BEARER = "Bearer "; public static final String CHARACTER_ENCODING = "UTF-8"; + public static final String ADMIN_ROLE = "ADMIN"; } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java index 5da40648..f30eba8e 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java @@ -1,5 +1,7 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtConstants.ADMIN_ROLE; + import io.jsonwebtoken.Header; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; @@ -22,20 +24,34 @@ public class JwtGenerator { private Long ACCESS_TOKEN_EXPIRATION_TIME; @Value("${jwt.refresh-token-expiration-time}") private Long REFRESH_TOKEN_EXPIRATION_TIME; + @Value("${jwt.admin-access-token-expiration-time}") + private Long ADMIN_ACCESS_TOKEN_EXPIRATION_TIME; - public String generateToken(Long userId, boolean isRefreshToken) { + public String generateToken(String subjectId, boolean isRefreshToken) { final Date now = generateNowDate(); final Date expiration = generateExpirationDate(isRefreshToken, now); return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) - .setSubject(String.valueOf(userId)) + .setSubject(subjectId) .setIssuedAt(now) .setExpiration(expiration) .signWith(getSigningKey()) .compact(); } + public String generateAdminToken() { + final Date now = generateNowDate(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setSubject(ADMIN_ROLE) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + ADMIN_ACCESS_TOKEN_EXPIRATION_TIME)) + .signWith(getSigningKey()) + .compact(); + } + public JwtParser getJwtParser() { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtPrefixExtractor.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtPrefixExtractor.java new file mode 100644 index 00000000..bf825e6b --- /dev/null +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtPrefixExtractor.java @@ -0,0 +1,19 @@ +package sopt.org.hmh.global.auth.jwt; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; +import sopt.org.hmh.global.auth.jwt.exception.JwtError; +import sopt.org.hmh.global.auth.jwt.exception.JwtException; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JwtPrefixExtractor { + + public static String extractPrefix(String accessToken) { + if (StringUtils.hasText(accessToken) && accessToken.startsWith(JwtConstants.BEARER)) { + return accessToken.substring(JwtConstants.BEARER.length()); + } + throw new JwtException(JwtError.INVALID_ACCESS_TOKEN); + } + +} diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java index dceb8aba..ade8b1df 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java @@ -1,5 +1,7 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtPrefixExtractor.extractPrefix; + import io.jsonwebtoken.JwtParser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -10,15 +12,20 @@ public class JwtProvider { private final JwtGenerator jwtGenerator; - public TokenResponse issueToken(Long userId) { - return TokenResponse.of(jwtGenerator.generateToken(userId, false), - jwtGenerator.generateToken(userId, true)); + public TokenResponse issueToken(String subjectId) { + return new TokenResponse(jwtGenerator.generateToken(subjectId, false), + jwtGenerator.generateToken(subjectId, true)); } - public Long getSubject(String token) { + public String getSubject(String token) { + String extractedToken = extractPrefix(token); JwtParser jwtParser = jwtGenerator.getJwtParser(); - return Long.valueOf(jwtParser.parseClaimsJws(token) + return jwtParser.parseClaimsJws(extractedToken) .getBody() - .getSubject()); + .getSubject(); + } + + public String issueAdminToken() { + return jwtGenerator.generateAdminToken(); } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java index b8ed8b24..c964c791 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java @@ -1,6 +1,11 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtConstants.ADMIN_ROLE; +import static sopt.org.hmh.global.auth.jwt.JwtPrefixExtractor.extractPrefix; + +import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtParser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -32,8 +37,16 @@ public void validateRefreshToken(String refreshToken) { } } - private void parseToken(String token) { + private Jws parseToken(String token) { + String extractedToken = extractPrefix(token); JwtParser jwtParser = jwtGenerator.getJwtParser(); - jwtParser.parseClaimsJws(token); + return jwtParser.parseClaimsJws(extractedToken); + } + + public void validateAdminToken(String token) { + String subject = parseToken(token).getBody().getSubject(); + if (!subject.equals(ADMIN_ROLE)) { + throw new JwtException(JwtError.INVALID_ADMIN_TOKEN); + } } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/TokenResponse.java b/src/main/java/sopt/org/hmh/global/auth/jwt/TokenResponse.java index d23e0086..a0751843 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/TokenResponse.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/TokenResponse.java @@ -4,7 +4,4 @@ public record TokenResponse( String accessToken, String refreshToken ) { - public static TokenResponse of(String accessToken, String refreshToken) { - return new TokenResponse(accessToken, refreshToken); - } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java b/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java index 64e2f6b9..b589abac 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java @@ -1,11 +1,11 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtPrefixExtractor.extractPrefix; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sopt.org.hmh.domain.auth.dto.response.ReissueResponse; -import sopt.org.hmh.global.auth.jwt.exception.JwtError; -import sopt.org.hmh.global.auth.jwt.exception.JwtException; @Service @RequiredArgsConstructor @@ -16,22 +16,22 @@ public class TokenService { @Transactional public ReissueResponse reissueToken(String refreshToken) { - String parsedRefreshToken = parseTokenString(refreshToken); - Long userId = jwtProvider.getSubject(parsedRefreshToken); + String parsedRefreshToken = extractPrefix(refreshToken); + String userId = jwtProvider.getSubject(parsedRefreshToken); jwtValidator.validateRefreshToken(parsedRefreshToken); return ReissueResponse.of(jwtProvider.issueToken(userId)); } - private String parseTokenString(String tokenString) { - String[] parsedTokens = tokenString.split(" "); - if (parsedTokens.length != 2) { - throw new JwtException(JwtError.INVALID_TOKEN_HEADER); - } - return parsedTokens[1]; + public TokenResponse issueToken(String subjectId) { + return jwtProvider.issueToken(subjectId); + } + + public String issueAdminToken() { + return jwtProvider.issueAdminToken(); } - public TokenResponse issueToken(Long userId) { - return jwtProvider.issueToken(userId); + public void validateAdminToken(String token){ + jwtValidator.validateAdminToken(token); } } diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java b/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java index 845d98a6..9006d88c 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java @@ -26,11 +26,14 @@ public enum JwtError implements ErrorBase { INVALID_IDENTITY_TOKEN_CLAIMS(HttpStatus.UNAUTHORIZED, "유효하지 않은 애플 아이덴티티 토큰 클레임입니다."), UNABLE_TO_CREATE_APPLE_PUBLIC_KEY(HttpStatus.UNAUTHORIZED, "애플 로그인 중 퍼블릭 키 생성에 문제가 발생했습니다."), + INVALID_ADMIN_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 관리자 액세스 토큰입니다."), + // 404 NOT FOUND NOT_FOUND_REFRESH_TOKEN_ERROR(HttpStatus.NOT_FOUND, "존재하지 않는 리프레시 토큰입니다."), // 500 INTERNAL ERROR INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."); + private final HttpStatus status; private final String errorMessage; diff --git a/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java b/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java index cffc553e..db01f72f 100644 --- a/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java +++ b/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java @@ -11,14 +11,11 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import sopt.org.hmh.global.auth.jwt.JwtConstants; import sopt.org.hmh.global.auth.jwt.JwtProvider; import sopt.org.hmh.global.auth.jwt.JwtValidator; -import sopt.org.hmh.global.auth.jwt.exception.JwtError; -import sopt.org.hmh.global.auth.jwt.exception.JwtException; @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -34,15 +31,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private String getAccessToken(HttpServletRequest request) { - String accessToken = request.getHeader(JwtConstants.AUTHORIZATION); - if (StringUtils.hasText(accessToken) && accessToken.startsWith(JwtConstants.BEARER)) { - return accessToken.substring(JwtConstants.BEARER.length()); - } - throw new JwtException(JwtError.INVALID_ACCESS_TOKEN); + return request.getHeader(JwtConstants.AUTHORIZATION); } - private void doAuthentication(HttpServletRequest request, Long userId) { - UserAuthentication authentication = createUserAuthentication(userId); + private void doAuthentication(HttpServletRequest request, String subjectId) { + UserAuthentication authentication = createUserAuthentication(subjectId); createAndSetWebAuthenticationDetails(request, authentication); SecurityContext securityContext = SecurityContextHolder.getContext(); securityContext.setAuthentication(authentication); diff --git a/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java b/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java index 7f5d5983..e3be2a1b 100644 --- a/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java +++ b/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java @@ -10,7 +10,7 @@ private UserAuthentication(Object principal, Object credentials, Collection resolvers) { resolvers.add(new UserIdArgumentResolver()); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(validateAdminInterceptor) + .addPathPatterns("/api/v1/admin/**") + .excludePathPatterns("/api/v1/admin/login"); + } } \ No newline at end of file