diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java b/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java index 62ada147..21a8ae35 100644 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java @@ -8,8 +8,8 @@ import com.weeth.domain.schedule.domain.service.EventSaveService; import com.weeth.domain.schedule.domain.service.EventUpdateService; import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.service.CardinalGetService; -import com.weeth.domain.user.domain.service.UserGetService; +import com.weeth.domain.user.domain.repository.CardinalReader; +import com.weeth.domain.user.domain.repository.UserReader; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,12 +20,12 @@ @RequiredArgsConstructor public class EventUseCaseImpl implements EventUseCase { - private final UserGetService userGetService; + private final UserReader userReader; private final EventGetService eventGetService; private final EventSaveService eventSaveService; private final EventUpdateService eventUpdateService; private final EventDeleteService eventDeleteService; - private final CardinalGetService cardinalGetService; + private final CardinalReader cardinalReader; private final EventMapper mapper; @Override @@ -36,8 +36,8 @@ public Response find(Long eventId) { @Override @Transactional public void save(ScheduleDTO.Save dto, Long userId) { - User user = userGetService.find(userId); - cardinalGetService.findByUserSide(dto.cardinal()); + User user = userReader.getById(userId); + cardinalReader.getByCardinalNumber(dto.cardinal()); eventSaveService.save(mapper.from(dto, user)); } @@ -45,7 +45,7 @@ public void save(ScheduleDTO.Save dto, Long userId) { @Override @Transactional public void update(Long eventId, ScheduleDTO.Update dto, Long userId) { - User user = userGetService.find(userId); + User user = userReader.getById(userId); Event event = eventGetService.find(eventId); eventUpdateService.update(event, dto, user); } diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java b/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java index 21a25151..de95fef2 100644 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java @@ -18,8 +18,10 @@ import com.weeth.domain.user.domain.entity.Cardinal; import com.weeth.domain.user.domain.entity.User; import com.weeth.domain.user.domain.entity.enums.Role; -import com.weeth.domain.user.domain.service.CardinalGetService; -import com.weeth.domain.user.domain.service.UserGetService; +import com.weeth.domain.user.domain.entity.enums.Status; +import com.weeth.domain.user.domain.repository.CardinalReader; +import com.weeth.domain.user.domain.repository.UserReader; +import com.weeth.domain.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -31,7 +33,6 @@ import java.util.Comparator; import java.util.List; -import static com.weeth.domain.schedule.application.dto.MeetingDTO.Info; import static com.weeth.domain.schedule.application.dto.MeetingDTO.Response; @Slf4j @@ -42,21 +43,22 @@ public class MeetingUseCaseImpl implements MeetingUseCase { private final MeetingGetService meetingGetService; private final MeetingMapper mapper; private final MeetingSaveService meetingSaveService; - private final UserGetService userGetService; + private final UserReader userReader; + private final UserRepository userRepository; private final MeetingUpdateService meetingUpdateService; private final MeetingDeleteService meetingDeleteService; private final AttendanceGetService attendanceGetService; private final AttendanceSaveService attendanceSaveService; private final AttendanceDeleteService attendanceDeleteService; private final AttendanceUpdateService attendanceUpdateService; - private final CardinalGetService cardinalGetService; + private final CardinalReader cardinalReader; @PersistenceContext private EntityManager em; @Override public Response find(Long userId, Long meetingId) { - User user = userGetService.find(userId); + User user = userReader.getById(userId); Meeting meeting = meetingGetService.find(meetingId); if (Role.ADMIN == user.getRole()) { @@ -87,10 +89,10 @@ public MeetingDTO.Infos find(Integer cardinal) { @Override @Transactional public void save(ScheduleDTO.Save dto, Long userId) { - User user = userGetService.find(userId); - Cardinal cardinal = cardinalGetService.findByUserSide(dto.cardinal()); + User user = userReader.getById(userId); + Cardinal cardinal = cardinalReader.getByCardinalNumber(dto.cardinal()); - List userList = userGetService.findAllByCardinal(cardinal); + List userList = userRepository.findAllByCardinalAndStatus(cardinal, Status.ACTIVE); Meeting meeting = mapper.from(dto, user); meetingSaveService.save(meeting); @@ -102,7 +104,7 @@ public void save(ScheduleDTO.Save dto, Long userId) { @Transactional public void update(ScheduleDTO.Update dto, Long userId, Long meetingId) { Meeting meeting = meetingGetService.find(meetingId); - User user = userGetService.find(userId); + User user = userReader.getById(userId); meetingUpdateService.update(dto, user, meeting); } diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java b/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java index c7818d1d..79fdcb36 100644 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java @@ -3,7 +3,8 @@ import com.weeth.domain.schedule.domain.service.EventGetService; import com.weeth.domain.schedule.domain.service.MeetingGetService; import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.service.CardinalGetService; +import com.weeth.domain.user.application.exception.CardinalNotFoundException; +import com.weeth.domain.user.domain.repository.CardinalRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -21,7 +22,7 @@ public class ScheduleUseCaseImpl implements ScheduleUseCase { private final EventGetService eventGetService; private final MeetingGetService meetingGetService; - private final CardinalGetService cardinalGetService; + private final CardinalRepository cardinalRepository; @Override public List findByMonthly(LocalDateTime start, LocalDateTime end) { @@ -36,7 +37,8 @@ public List findByMonthly(LocalDateTime start, LocalDateTime end) { @Override public Map> findByYearly(Integer year, Integer semester) { - Cardinal cardinal = cardinalGetService.find(year, semester); + Cardinal cardinal = cardinalRepository.findByYearAndSemester(year, semester) + .orElseThrow(CardinalNotFoundException::new); List events = eventGetService.find(cardinal.getCardinalNumber()); List meetings = meetingGetService.findByCardinal(cardinal.getCardinalNumber()); diff --git a/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java b/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java index e85d650f..8b3c1128 100644 --- a/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java +++ b/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java @@ -17,6 +17,8 @@ public interface MeetingRepository extends JpaRepository { List findAllByCardinal(int cardinal); + List findAllByCardinalInOrderByCardinalAscStartAsc(List cardinals); + List findAllByMeetingStatusAndEndBeforeOrderByEndAsc(MeetingStatus status, LocalDateTime end); List findAllByOrderByStartDesc(); diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java b/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java index 3eab97d2..6eebc011 100644 --- a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java +++ b/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java @@ -10,7 +10,10 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -34,6 +37,14 @@ public List find(Integer cardinal) { return meetingRepository.findAllByCardinalOrderByStartAsc(cardinal); } + public Map> findByCardinals(List cardinals) { + if (cardinals == null || cardinals.isEmpty()) { + return Map.of(); + } + return meetingRepository.findAllByCardinalInOrderByCardinalAscStartAsc(cardinals).stream() + .collect(Collectors.groupingBy(Meeting::getCardinal, LinkedHashMap::new, Collectors.toList())); + } + public List findMeetingByCardinal(Integer cardinal) { return meetingRepository.findAllByCardinalOrderByStartDesc(cardinal); } diff --git a/src/main/java/com/weeth/domain/user/application/dto/request/CardinalSaveRequest.java b/src/main/java/com/weeth/domain/user/application/dto/request/CardinalSaveRequest.java deleted file mode 100644 index 3896b1aa..00000000 --- a/src/main/java/com/weeth/domain/user/application/dto/request/CardinalSaveRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.weeth.domain.user.application.dto.request; - -import jakarta.validation.constraints.NotNull; - -public record CardinalSaveRequest ( - @NotNull Integer cardinalNumber, - @NotNull Integer year, - @NotNull Integer semester, - boolean inProgress -){ -} diff --git a/src/main/java/com/weeth/domain/user/application/dto/request/CardinalUpdateRequest.java b/src/main/java/com/weeth/domain/user/application/dto/request/CardinalUpdateRequest.java deleted file mode 100644 index 029d7154..00000000 --- a/src/main/java/com/weeth/domain/user/application/dto/request/CardinalUpdateRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.weeth.domain.user.application.dto.request; - -import jakarta.validation.constraints.NotNull; - -public record CardinalUpdateRequest( - @NotNull Long id, - @NotNull Integer year, - @NotNull Integer semester, - boolean inProgress -) { -} diff --git a/src/main/java/com/weeth/domain/user/application/dto/request/UserRequestDto.java b/src/main/java/com/weeth/domain/user/application/dto/request/UserRequestDto.java deleted file mode 100644 index 30e16e42..00000000 --- a/src/main/java/com/weeth/domain/user/application/dto/request/UserRequestDto.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.weeth.domain.user.application.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import com.weeth.domain.user.domain.entity.enums.Role; - -import java.util.List; - -public class UserRequestDto { - - public record Login( - @NotBlank String authCode - ) { - } - - public record SignUp( - @NotBlank String name, - @Email @NotBlank String email, - @NotBlank String password, - @NotBlank String studentId, - @NotBlank String tel, - @NotNull String position, - @NotNull String department, - @NotNull Integer cardinal - ) { - } - - public record Register( - @Schema(description = "kakao로 회원가입 하는 경우") - Long kakaoId, - @Schema(description = "애플로 회원가입 하는 경우 - Apple OAuth authCode") - String appleAuthCode, - @NotBlank String name, - @NotBlank String studentId, - @NotBlank String email, - @NotNull String department, - @NotBlank String tel, - @NotNull Integer cardinal, - @NotNull String position - ) { - } - - public record Update( - @NotBlank String name, - @Email @NotBlank String email, - @NotBlank String studentId, - @NotBlank String tel, - @NotNull String department - ) { - } - - public record NormalLogin( - @Email @NotBlank String email, - @NotBlank String passWord, - @NotNull Long kakaoId - ) { - } - - public record UserRoleUpdate( - @NotNull Long userId, - @NotNull Role role - ) { - } - - public record UserApplyOB( - @NotNull Long userId, - @NotNull Integer cardinal - ) { - } - - public record UserId( - @NotNull List userId - ) { - } - -} diff --git a/src/main/java/com/weeth/domain/user/application/dto/response/CardinalResponse.java b/src/main/java/com/weeth/domain/user/application/dto/response/CardinalResponse.java deleted file mode 100644 index c296be9f..00000000 --- a/src/main/java/com/weeth/domain/user/application/dto/response/CardinalResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.weeth.domain.user.application.dto.response; - -import com.weeth.domain.user.domain.entity.enums.CardinalStatus; - -import java.time.LocalDateTime; - -public record CardinalResponse( - Long id, - Integer cardinalNumber, - Integer year, - Integer semester, - CardinalStatus status, - LocalDateTime createdAt, - LocalDateTime modifiedAt -) { -} diff --git a/src/main/java/com/weeth/domain/user/application/dto/response/UserCardinalDto.java b/src/main/java/com/weeth/domain/user/application/dto/response/UserCardinalDto.java deleted file mode 100644 index a7f2f6fb..00000000 --- a/src/main/java/com/weeth/domain/user/application/dto/response/UserCardinalDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.weeth.domain.user.application.dto.response; - -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; - -import java.util.List; - -public record UserCardinalDto( - User user, - List cardinals -) { -} diff --git a/src/main/java/com/weeth/domain/user/application/dto/response/UserResponseDto.java b/src/main/java/com/weeth/domain/user/application/dto/response/UserResponseDto.java deleted file mode 100644 index 9ec2b286..00000000 --- a/src/main/java/com/weeth/domain/user/application/dto/response/UserResponseDto.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.weeth.domain.user.application.dto.response; - -import com.weeth.domain.user.domain.entity.enums.LoginStatus; -import com.weeth.domain.user.domain.entity.enums.Position; -import com.weeth.domain.user.domain.entity.enums.Role; -import com.weeth.domain.user.domain.entity.enums.Status; - -import java.time.LocalDateTime; -import java.util.List; - -public class UserResponseDto { - - public record SocialLoginResponse( - Long id, - Long kakaoId, - String appleIdToken, - LoginStatus status, - String accessToken, - String refreshToken - ) { - } - - public record Response( - Integer id, - String name, - String email, - String studentId, - String tel, - String department, - List cardinals, - Position position, - Role role - ) { - } - - public record SummaryResponse( - Integer id, - String name, - List cardinals, - Position position, - Role role - ) { - } - - public record AdminResponse( - Integer id, - String name, - String email, - String studentId, - String tel, - String department, - List cardinals, - Position position, - Status status, - Role role, - Integer attendanceCount, - Integer absenceCount, - Integer attendanceRate, - Integer penaltyCount, - Integer warningCount, - LocalDateTime createdAt, - LocalDateTime modifiedAt - ) { - } - - public record UserResponse( - Integer id, - String name, - String email, - String studentId, - String department, - List cardinals, - Position position, - Role role - ) { - } - - public record SocialAuthResponse( - Long kakaoId - ) { - } - - public record UserInfo( - Long id, - String name, - List cardinals, - Role role - ) { - } -} //todo: User 전역 dto 구현 (id, 이름, role) diff --git a/src/main/java/com/weeth/domain/user/application/exception/CardinalNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/CardinalNotFoundException.java deleted file mode 100644 index fb4568e9..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/CardinalNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class CardinalNotFoundException extends BaseException { - public CardinalNotFoundException() { - super(UserErrorCode.CARDINAL_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/DepartmentNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/DepartmentNotFoundException.java deleted file mode 100644 index bf8abbbb..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/DepartmentNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class DepartmentNotFoundException extends BaseException { - public DepartmentNotFoundException() { - super(UserErrorCode.DEPARTMENT_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/DuplicateCardinalException.java b/src/main/java/com/weeth/domain/user/application/exception/DuplicateCardinalException.java deleted file mode 100644 index 02646132..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/DuplicateCardinalException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class DuplicateCardinalException extends BaseException { - public DuplicateCardinalException() { - super(UserErrorCode.DUPLICATE_CARDINAL); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/EmailNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/EmailNotFoundException.java deleted file mode 100644 index 69b9fda5..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/EmailNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class EmailNotFoundException extends BaseException { - public EmailNotFoundException() { - super(UserErrorCode.EMAIL_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/InvalidUserOrderException.java b/src/main/java/com/weeth/domain/user/application/exception/InvalidUserOrderException.java deleted file mode 100644 index 179dc19f..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/InvalidUserOrderException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class InvalidUserOrderException extends BaseException { - public InvalidUserOrderException() { - super(UserErrorCode.INVALID_USER_ORDER); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/PasswordMismatchException.java b/src/main/java/com/weeth/domain/user/application/exception/PasswordMismatchException.java deleted file mode 100644 index cb5af50e..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/PasswordMismatchException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class PasswordMismatchException extends BaseException { - public PasswordMismatchException() { - super(UserErrorCode.PASSWORD_MISMATCH); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/RoleNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/RoleNotFoundException.java deleted file mode 100644 index 9bfe4c15..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/RoleNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class RoleNotFoundException extends BaseException { - public RoleNotFoundException() { - super(UserErrorCode.ROLE_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/StatusNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/StatusNotFoundException.java deleted file mode 100644 index f09d7fd1..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/StatusNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class StatusNotFoundException extends BaseException { - public StatusNotFoundException() { - super(UserErrorCode.STATUS_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/StudentIdExistsException.java b/src/main/java/com/weeth/domain/user/application/exception/StudentIdExistsException.java deleted file mode 100644 index 4c9e3271..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/StudentIdExistsException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class StudentIdExistsException extends BaseException { - public StudentIdExistsException() { - super(UserErrorCode.STUDENT_ID_EXISTS); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/TelExistsException.java b/src/main/java/com/weeth/domain/user/application/exception/TelExistsException.java deleted file mode 100644 index 53e613e6..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/TelExistsException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class TelExistsException extends BaseException { - public TelExistsException() { - super(UserErrorCode.TEL_EXISTS); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserCardinalNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/UserCardinalNotFoundException.java deleted file mode 100644 index cf785d5e..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/UserCardinalNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class UserCardinalNotFoundException extends BaseException { - public UserCardinalNotFoundException() { - super(UserErrorCode.USER_CARDINAL_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserExistsException.java b/src/main/java/com/weeth/domain/user/application/exception/UserExistsException.java deleted file mode 100644 index d14b6f37..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/UserExistsException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class UserExistsException extends BaseException { - public UserExistsException() { - super(UserErrorCode.USER_EXISTS); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserInActiveException.java b/src/main/java/com/weeth/domain/user/application/exception/UserInActiveException.java deleted file mode 100644 index 2090edb5..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/UserInActiveException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class UserInActiveException extends BaseException { - public UserInActiveException() { - super(UserErrorCode.USER_INACTIVE); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserMismatchException.java b/src/main/java/com/weeth/domain/user/application/exception/UserMismatchException.java deleted file mode 100644 index e63db955..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/UserMismatchException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class UserMismatchException extends BaseException { - public UserMismatchException() { - super(UserErrorCode.USER_MISMATCH); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserNotFoundException.java b/src/main/java/com/weeth/domain/user/application/exception/UserNotFoundException.java deleted file mode 100644 index a4fbd495..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/UserNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class UserNotFoundException extends BaseException { - public UserNotFoundException() { - super(UserErrorCode.USER_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserNotMatchException.java b/src/main/java/com/weeth/domain/user/application/exception/UserNotMatchException.java deleted file mode 100644 index b7ff871f..00000000 --- a/src/main/java/com/weeth/domain/user/application/exception/UserNotMatchException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class UserNotMatchException extends BaseException { - public UserNotMatchException() { - super(UserErrorCode.USER_NOT_MATCH); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/mapper/CardinalMapper.java b/src/main/java/com/weeth/domain/user/application/mapper/CardinalMapper.java deleted file mode 100644 index ca39e9da..00000000 --- a/src/main/java/com/weeth/domain/user/application/mapper/CardinalMapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.weeth.domain.user.application.mapper; - -import com.weeth.domain.user.application.dto.request.CardinalSaveRequest; -import com.weeth.domain.user.application.dto.response.CardinalResponse; -import com.weeth.domain.user.application.dto.response.UserCardinalDto; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; -import org.mapstruct.Mapper; -import org.mapstruct.MappingConstants; -import org.mapstruct.ReportingPolicy; - -import java.util.List; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface CardinalMapper { - - Cardinal from(CardinalSaveRequest dto); - - CardinalResponse to(Cardinal cardinal); - - UserCardinalDto toUserCardinalDto(User user, List cardinals); -} diff --git a/src/main/java/com/weeth/domain/user/application/mapper/UserMapper.java b/src/main/java/com/weeth/domain/user/application/mapper/UserMapper.java deleted file mode 100644 index a0f36096..00000000 --- a/src/main/java/com/weeth/domain/user/application/mapper/UserMapper.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.weeth.domain.user.application.mapper; - -import com.weeth.domain.user.application.dto.response.UserResponseDto; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; -import com.weeth.domain.user.domain.entity.enums.Department; -import com.weeth.global.auth.jwt.application.dto.JwtDto; -import org.mapstruct.*; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.Register; -import static com.weeth.domain.user.application.dto.request.UserRequestDto.SignUp; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.*; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface UserMapper { - - @Mappings({ - @Mapping(target = "password", expression = "java( passwordEncoder.encode(dto.password()) )"), - @Mapping(target = "department", expression = "java( com.weeth.domain.user.domain.entity.enums.Department.to(dto.department()) )") - }) - User from(SignUp dto, @Context PasswordEncoder passwordEncoder); - - @Mappings({ - @Mapping(target = "department", expression = "java( com.weeth.domain.user.domain.entity.enums.Department.to(dto.department()) )") - }) - User from(Register dto); - - @Mapping(target = "department", expression = "java( toString(user.getDepartment()) )") - @Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )") - Response to(User user, List userCardinals); - - @Mappings({ - // 수정: 출석률, 출석 횟수, 결석 횟수 매핑 추후 추가 예정 - @Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )") - }) - AdminResponse toAdminResponse(User user, List userCardinals); - - @Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )") - SummaryResponse toSummaryResponse(User user, List userCardinals); - - SocialAuthResponse toSocialAuthResponse(Long kakaoId); - - @Mappings({ - @Mapping(target = "status", expression = "java(LoginStatus.LOGIN)"), - @Mapping(target = "id", source = "user.id"), - @Mapping(target = "kakaoId", source = "user.kakaoId"), - @Mapping(target = "appleIdToken", expression = "java(null)") - }) - SocialLoginResponse toLoginResponse(User user, JwtDto dto); - - @Mappings({ - @Mapping(target = "status", expression = "java(LoginStatus.INTEGRATE)"), - @Mapping(target = "appleIdToken", expression = "java(null)"), - @Mapping(target = "accessToken", expression = "java(null)"), - @Mapping(target = "refreshToken", expression = "java(null)") - }) - SocialLoginResponse toIntegrateResponse(Long kakaoId); - - @Mappings({ - // 상세 데이터 매핑 - @Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )") - }) - UserResponse toUserResponse(User user, List userCardinals); - - @Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )") - UserResponseDto.UserInfo toUserInfoDto(User user, List userCardinals); - - @Mappings({ - @Mapping(target = "status", expression = "java(LoginStatus.LOGIN)"), - @Mapping(target = "id", source = "user.id"), - @Mapping(target = "appleIdToken", expression = "java(null)"), - @Mapping(target = "kakaoId", expression = "java(null)") - }) - SocialLoginResponse toAppleLoginResponse(User user, JwtDto dto); - - @Mappings({ - @Mapping(target = "status", expression = "java(LoginStatus.INTEGRATE)"), - @Mapping(target = "id", expression = "java(null)"), - @Mapping(target = "appleIdToken", source = "appleIdToken"), - @Mapping(target = "kakaoId", expression = "java(null)"), - @Mapping(target = "accessToken", expression = "java(null)"), - @Mapping(target = "refreshToken", expression = "java(null)") - }) - SocialLoginResponse toAppleIntegrateResponse(String appleIdToken); - - default String toString(Department department) { - return department.getValue(); - } - - default List toCardinalNumbers(List userCardinals) { - if (userCardinals == null || userCardinals.isEmpty()) { - return Collections.emptyList(); - } - - return userCardinals.stream() - .map(uc -> uc.getCardinal().getCardinalNumber()) - .collect(Collectors.toList()); - } -} - diff --git a/src/main/java/com/weeth/domain/user/application/usecase/CardinalUseCase.java b/src/main/java/com/weeth/domain/user/application/usecase/CardinalUseCase.java deleted file mode 100644 index 1723054e..00000000 --- a/src/main/java/com/weeth/domain/user/application/usecase/CardinalUseCase.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.weeth.domain.user.application.usecase; - -import com.weeth.domain.user.application.dto.request.CardinalSaveRequest; -import com.weeth.domain.user.application.dto.request.CardinalUpdateRequest; -import com.weeth.domain.user.application.dto.response.CardinalResponse; -import com.weeth.domain.user.application.mapper.CardinalMapper; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.service.CardinalGetService; -import com.weeth.domain.user.domain.service.CardinalSaveService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class CardinalUseCase { - - private final CardinalGetService cardinalGetService; - private final CardinalSaveService cardinalSaveService; - - private final CardinalMapper cardinalMapper; - - @Transactional - public void save(CardinalSaveRequest dto) { - cardinalGetService.validateCardinal(dto.cardinalNumber()); - - Cardinal cardinal = cardinalSaveService.save(cardinalMapper.from(dto)); - - if (dto.inProgress()) { - updateCardinalStatus(cardinal); - } - } - - @Transactional - public void update(CardinalUpdateRequest dto) { - Cardinal cardinal = cardinalGetService.findById(dto.id()); - - cardinal.update(dto); - - if (dto.inProgress()) { - updateCardinalStatus(cardinal); - } - } - - public List findAll() { - List cardinals = cardinalGetService.findAll(); - return cardinals.stream() - .map(cardinalMapper::to) - .toList(); - } - - private void updateCardinalStatus(Cardinal cardinal) { - List cardinals = cardinalGetService.findInProgress(); - - if (!cardinals.isEmpty()) { - cardinals.forEach(Cardinal::done); - } - - cardinal.inProgress(); - } -} diff --git a/src/main/java/com/weeth/domain/user/application/usecase/UserManageUseCase.java b/src/main/java/com/weeth/domain/user/application/usecase/UserManageUseCase.java deleted file mode 100644 index 7e3bc11f..00000000 --- a/src/main/java/com/weeth/domain/user/application/usecase/UserManageUseCase.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.weeth.domain.user.application.usecase; - -import com.weeth.domain.user.application.dto.response.UserResponseDto; -import com.weeth.domain.user.domain.entity.enums.UsersOrderBy; - -import java.util.List; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.*; - -public interface UserManageUseCase { - - - List findAllByAdmin(UsersOrderBy orderBy); - - void accept(UserId userIds); - - void update(List request); - - void leave(Long userId); - - void ban(UserId userIds); - - void applyOB(List request); - - void reset(UserId userId); -} diff --git a/src/main/java/com/weeth/domain/user/application/usecase/UserManageUseCaseImpl.java b/src/main/java/com/weeth/domain/user/application/usecase/UserManageUseCaseImpl.java deleted file mode 100644 index aa194a5d..00000000 --- a/src/main/java/com/weeth/domain/user/application/usecase/UserManageUseCaseImpl.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.weeth.domain.user.application.usecase; - -import jakarta.transaction.Transactional; -import com.weeth.domain.attendance.domain.service.AttendanceSaveService; -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.schedule.domain.service.MeetingGetService; -import com.weeth.domain.user.application.exception.InvalidUserOrderException; -import com.weeth.domain.user.application.mapper.UserMapper; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; -import com.weeth.domain.user.domain.entity.enums.StatusPriority; -import com.weeth.domain.user.domain.entity.enums.UsersOrderBy; -import com.weeth.domain.user.domain.service.*; -import com.weeth.global.auth.jwt.domain.port.RefreshTokenStorePort; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.stream.Collectors; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.*; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.AdminResponse; -import static com.weeth.domain.user.domain.entity.enums.UsersOrderBy.CARDINAL_DESCENDING; -import static com.weeth.domain.user.domain.entity.enums.UsersOrderBy.NAME_ASCENDING; - -@Service -@RequiredArgsConstructor -public class UserManageUseCaseImpl implements UserManageUseCase { - - private final UserGetService userGetService; - private final UserUpdateService userUpdateService; - private final UserDeleteService userDeleteService; - - private final AttendanceSaveService attendanceSaveService; - private final MeetingGetService meetingGetService; - private final RefreshTokenStorePort refreshTokenStorePort; - private final CardinalGetService cardinalGetService; - private final UserCardinalSaveService userCardinalSaveService; - private final UserCardinalGetService userCardinalGetService; - - private final UserMapper mapper; - private final PasswordEncoder passwordEncoder; - - @Override - public List findAllByAdmin(UsersOrderBy orderBy) { - if (orderBy == null || !EnumSet.allOf(UsersOrderBy.class).contains(orderBy)) { - throw new InvalidUserOrderException(); - } - - Map> userCardinalMap = userCardinalGetService.findAll() - .stream() - .collect(Collectors.groupingBy(UserCardinal::getUser, LinkedHashMap::new, Collectors.toList())); - - if (orderBy.equals(NAME_ASCENDING)) { - return userCardinalMap.entrySet() - .stream() - .sorted(Comparator - .comparingInt(((Map.Entry> entry) -> (StatusPriority.fromStatus(entry.getKey().getStatus())).getPriority()))) - .map(entry -> { - List userCardinals = userCardinalGetService.getUserCardinals(entry.getKey()); - return mapper.toAdminResponse(entry.getKey(), userCardinals); - }) - .toList(); - } - - if (orderBy.equals(CARDINAL_DESCENDING)) { - - return userCardinalMap.entrySet() - .stream() - .sorted(Comparator - .comparingInt(((Map.Entry> entry) -> (StatusPriority.fromStatus(entry.getKey().getStatus())).getPriority())) - .thenComparing(entry -> entry.getValue().stream() - .map(uc -> uc.getCardinal().getCardinalNumber()) - .max(Integer::compare) - .orElse(-1), Comparator.reverseOrder())) - .map(entry -> { - List userCardinals = userCardinalGetService.getUserCardinals(entry.getKey()); - return mapper.toAdminResponse(entry.getKey(), userCardinals); - }) - .toList(); - } - - return null; - } - - @Override - @Transactional - public void accept(UserId userIds) { - List users = userGetService.findAll(userIds.userId()); - - users.forEach(user -> { - Integer cardinal = userCardinalGetService.getCurrentCardinal(user).getCardinalNumber(); - - if (user.isInactive()) { - userUpdateService.accept(user); - List meetings = meetingGetService.find(cardinal); - attendanceSaveService.init(user, meetings); - } - }); - } - - @Override - @Transactional - public void update(List requests) { - requests.forEach(request -> { - User user = userGetService.find(request.userId()); - - userUpdateService.update(user, request.role().name()); - refreshTokenStorePort.updateRole(user.getId(), request.role()); - }); - } - - @Override - public void leave(Long userId) { - User user = userGetService.find(userId); - // 탈퇴하는 경우 리프레시 토큰 삭제 - refreshTokenStorePort.delete(user.getId()); - userDeleteService.leave(user); - } - - @Override - public void ban(UserId userIds) { - List users = userGetService.findAll(userIds.userId()); - - users.forEach(user -> { - refreshTokenStorePort.delete(user.getId()); - userDeleteService.ban(user); - }); - } - - @Override - @Transactional - public void applyOB(List requests) { - requests.forEach(request -> { - User user = userGetService.find(request.userId()); - Cardinal nextCardinal = cardinalGetService.findByAdminSide(request.cardinal()); - - if (userCardinalGetService.notContains(user, nextCardinal)) { - if (userCardinalGetService.isCurrent(user, nextCardinal)) { - user.initAttendance(); - List meetings = meetingGetService.find(request.cardinal()); - attendanceSaveService.init(user, meetings); - } - UserCardinal userCardinal = new UserCardinal(user, nextCardinal); - - userCardinalSaveService.save(userCardinal); - } - }); - } - - @Override - @Transactional - public void reset(UserId userId) { - - List users = userGetService.findAll(userId.userId()); - - users.forEach(user -> userUpdateService.reset(user, passwordEncoder)); - } - -} diff --git a/src/main/java/com/weeth/domain/user/application/usecase/UserUseCase.java b/src/main/java/com/weeth/domain/user/application/usecase/UserUseCase.java deleted file mode 100644 index f549ea9d..00000000 --- a/src/main/java/com/weeth/domain/user/application/usecase/UserUseCase.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.weeth.domain.user.application.usecase; - -import com.weeth.domain.user.application.dto.request.UserRequestDto; -import com.weeth.domain.user.application.dto.response.UserResponseDto; -import com.weeth.global.auth.jwt.application.dto.JwtDto; -import org.springframework.data.domain.Slice; - -import java.util.List; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.*; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.*; - - -public interface UserUseCase { - - SocialLoginResponse login(Login dto); - - SocialAuthResponse authenticate(Login dto); - - SocialLoginResponse integrate(NormalLogin dto); - - UserResponseDto.Response find(Long userId); - - Slice findAllUser(int pageNumber, int pageSize, Integer cardinal); - - UserResponseDto.UserResponse findUserDetails(Long userId); - - void update(UserRequestDto.Update dto, Long userId); - - void apply(SignUp dto); - - void socialRegister(Register dto); - - JwtDto refresh(String refreshToken); - - UserResponseDto.UserInfo findUserInfo(Long userId); - - List searchUser(String keyword); - - SocialLoginResponse appleLogin(Login dto); - - void appleRegister(Register dto); - -} diff --git a/src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java b/src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java deleted file mode 100644 index 1702081b..00000000 --- a/src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.weeth.domain.user.application.usecase; - -import com.weeth.domain.user.application.dto.response.UserCardinalDto; -import com.weeth.domain.user.application.exception.PasswordMismatchException; -import com.weeth.domain.user.application.exception.StudentIdExistsException; -import com.weeth.domain.user.application.exception.TelExistsException; -import com.weeth.domain.user.application.exception.UserInActiveException; -import com.weeth.domain.user.application.mapper.CardinalMapper; -import com.weeth.domain.user.application.mapper.UserMapper; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; -import com.weeth.domain.user.domain.service.*; -import com.weeth.global.auth.apple.dto.AppleTokenResponse; -import com.weeth.global.auth.apple.dto.AppleUserInfo; -import com.weeth.global.auth.jwt.application.dto.JwtDto; -import com.weeth.global.auth.jwt.application.usecase.JwtManageUseCase; -import com.weeth.global.auth.kakao.KakaoAuthService; -import com.weeth.global.auth.kakao.dto.KakaoTokenResponse; -import com.weeth.global.auth.kakao.dto.KakaoUserInfoResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.env.Environment; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; -import java.util.stream.Collectors; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.*; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.*; - -@Slf4j -@Service -@RequiredArgsConstructor -public class UserUseCaseImpl implements UserUseCase { - private static final String BEARER = "Bearer "; - private final JwtManageUseCase jwtManageUseCase; - private final UserSaveService userSaveService; - private final UserGetService userGetService; - private final UserUpdateService userUpdateService; - private final KakaoAuthService kakaoAuthService; - private final com.weeth.global.auth.apple.AppleAuthService appleAuthService; - private final CardinalGetService cardinalGetService; - private final UserCardinalSaveService userCardinalSaveService; - private final UserCardinalGetService userCardinalGetService; - - private final UserMapper mapper; - private final CardinalMapper cardinalMapper; - private final PasswordEncoder passwordEncoder; - private final Environment environment; - - @Override - @Transactional(readOnly = true) - public SocialLoginResponse login(Login dto) { - long kakaoId = getKakaoId(dto); - Optional optionalUser = userGetService.findByKakaoId(kakaoId); - - if (optionalUser.isEmpty()) { - return mapper.toIntegrateResponse(kakaoId); - } - - User user = optionalUser.get(); - if (user.isInactive()) { - throw new UserInActiveException(); - } - - JwtDto token = jwtManageUseCase.create(user.getId(), user.getEmail(), user.getRole()); - return mapper.toLoginResponse(user, token); - } - - @Override - public SocialAuthResponse authenticate(Login dto) { - long kakaoId = getKakaoId(dto); - - return mapper.toSocialAuthResponse(kakaoId); - } - - @Override - @Transactional - public SocialLoginResponse integrate(NormalLogin dto) { - User user = userGetService.find(dto.email()); - - if (!passwordEncoder.matches(dto.passWord(), user.getPassword())) { - throw new PasswordMismatchException(); - } - user.addKakaoId(dto.kakaoId()); - - if (user.isInactive()) { - throw new UserInActiveException(); - } - - JwtDto token = jwtManageUseCase.create(user.getId(), user.getEmail(), user.getRole()); - - return mapper.toLoginResponse(user, token); - } - - @Override - public Slice findAllUser(int pageNumber, int pageSize, Integer cardinal) { - - Pageable pageable = PageRequest.of(pageNumber, pageSize); - Slice users; - - if (cardinal == null) { - users = userGetService.findAll(pageable); - - } else { - Cardinal inputCardinal = cardinalGetService.findByUserSide(cardinal); - users = userGetService.findAll(pageable, inputCardinal); - } - - List allUserCardinals = userCardinalGetService.findAll(users.getContent()); - - Map> userCardinalMap = allUserCardinals.stream() - .collect(Collectors.groupingBy(userCardinal -> userCardinal.getUser().getId())); - - return users.map(user -> { - List userCardinals = userCardinalMap.getOrDefault(user.getId(), Collections.emptyList()); - - return mapper.toSummaryResponse(user, userCardinals); - }); - } - - @Override - public UserResponse findUserDetails(Long userId) { - UserCardinalDto dto = getUserCardinalDto(userId); - - return mapper.toUserResponse(dto.user(), dto.cardinals()); - } - - @Override - public Response find(Long userId) { - UserCardinalDto dto = getUserCardinalDto(userId); - - return mapper.to(dto.user(), dto.cardinals()); - } - - @Override - public void update(Update dto, Long userId) { - validate(dto, userId); - User user = userGetService.find(userId); - userUpdateService.update(user, dto); - } - - @Override - @Transactional - public void apply(SignUp dto) { - validate(dto); - - Cardinal cardinal = cardinalGetService.findByUserSide(dto.cardinal()); - User user = mapper.from(dto, passwordEncoder); - UserCardinal userCardinal = new UserCardinal(user, cardinal); - - userSaveService.save(user); - userCardinalSaveService.save(userCardinal); - } - - @Override - @Transactional - public void socialRegister(Register dto) { - validate(dto); - - Cardinal cardinal = cardinalGetService.findByUserSide(dto.cardinal()); - - User user = mapper.from(dto); - UserCardinal userCardinal = new UserCardinal(user, cardinal); - - userSaveService.save(user); - userCardinalSaveService.save(userCardinal); - } - - @Override - @Transactional - public JwtDto refresh(String refreshToken) { - - String requestToken = refreshToken.replace(BEARER, ""); - - JwtDto token = jwtManageUseCase.reIssueToken(requestToken); - - log.info("RefreshToken 발급 완료: {}", token); - return new JwtDto(token.getAccessToken(), token.getRefreshToken()); - } - - @Override - public UserInfo findUserInfo(Long userId) { - UserCardinalDto dto = getUserCardinalDto(userId); - - return mapper.toUserInfoDto(dto.user(), dto.cardinals()); - } - - @Override - public List searchUser(String keyword) { - List users = userGetService.search(keyword); - - return users.stream() - .map(user -> { - List userCardinals = userCardinalGetService.getUserCardinals(user); - return mapper.toSummaryResponse(user, userCardinals); - }) - .toList(); - } - - private long getKakaoId(Login dto) { - KakaoTokenResponse tokenResponse = kakaoAuthService.getKakaoToken(dto.authCode()); - KakaoUserInfoResponse userInfo = kakaoAuthService.getUserInfo(tokenResponse.getAccessToken()); - - return userInfo.getId(); - } - - private void validate(Update dto, Long userId) { - if (userGetService.validateStudentId(dto.studentId(), userId)) - throw new StudentIdExistsException(); - if (userGetService.validateTel(dto.tel(), userId)) - throw new TelExistsException(); - } - - private void validate(SignUp dto) { - if (userGetService.validateStudentId(dto.studentId())) - throw new StudentIdExistsException(); - if (userGetService.validateTel(dto.tel())) - throw new TelExistsException(); - } - - private void validate(Register dto) { - if (userGetService.validateStudentId(dto.studentId())) { - throw new StudentIdExistsException(); - } - if (userGetService.validateTel(dto.tel())) { - throw new TelExistsException(); - } - } - - private UserCardinalDto getUserCardinalDto(Long userId) { - User user = userGetService.find(userId); - List userCardinals = userCardinalGetService.getUserCardinals(user); - - return cardinalMapper.toUserCardinalDto(user, userCardinals); - } - - @Override - @Transactional(readOnly = true) - public SocialLoginResponse appleLogin(Login dto) { - // Apple Token 요청 및 유저 정보 요청 - AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(dto.authCode()); - AppleUserInfo userInfo = appleAuthService.verifyAndDecodeIdToken(tokenResponse.getIdToken()); - - String appleIdToken = tokenResponse.getIdToken(); - String appleId = userInfo.getAppleId(); - - Optional optionalUser = userGetService.findByAppleId(appleId); - - //todo: 추후 애플 로그인 연동을 위해 appleIdToken을 반환 - // 애플 로그인 연동 API 요청시 appleIdToken을 함께 넣어주면 그때 디코딩해서 appleId를 추출 - if (optionalUser.isEmpty()) { - return mapper.toAppleIntegrateResponse(appleIdToken); - } - - User user = optionalUser.get(); - if (user.isInactive()) { - throw new UserInActiveException(); - } - - JwtDto token = jwtManageUseCase.create(user.getId(), user.getEmail(), user.getRole()); - return mapper.toAppleLoginResponse(user, token); - } - - @Override - @Transactional - public void appleRegister(Register dto) { - validate(dto); - - // Apple authCode로 토큰 교환 후 ID Token 검증 및 사용자 정보 추출 - AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(dto.appleAuthCode()); - AppleUserInfo appleUserInfo = appleAuthService.verifyAndDecodeIdToken(tokenResponse.getIdToken()); - - Cardinal cardinal = cardinalGetService.findByUserSide(dto.cardinal()); - - User user = mapper.from(dto); - // Apple ID 설정 - user.addAppleId(appleUserInfo.getAppleId()); - - UserCardinal userCardinal = new UserCardinal(user, cardinal); - - userSaveService.save(user); - userCardinalSaveService.save(userCardinal); - - // dev 환경에서만 바로 ACTIVE 상태로 설정 - if (isDevEnvironment()) { - log.info("dev 환경 감지: 사용자 자동 승인 처리 (userId: {})", user.getId()); - user.accept(); - } - } - - /** - * 현재 환경이 dev 프로파일인지 확인 - * @return dev 프로파일이 활성화되어 있으면 true - */ - private boolean isDevEnvironment() { - String[] activeProfiles = environment.getActiveProfiles(); - for (String profile : activeProfiles) { - if ("dev".equals(profile)) { - return true; - } - if ("local".equals(profile)) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/Cardinal.java b/src/main/java/com/weeth/domain/user/domain/entity/Cardinal.java deleted file mode 100644 index 942e94e0..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/Cardinal.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.weeth.domain.user.domain.entity; - -import jakarta.persistence.*; -import com.weeth.domain.user.application.dto.request.CardinalUpdateRequest; -import com.weeth.domain.user.domain.entity.enums.CardinalStatus; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@SuperBuilder -public class Cardinal extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "cardinal_id") - private Long id; - - @Column(unique = true, nullable = false) - private Integer cardinalNumber; - - private Integer year; - - private Integer semester; - - @Builder.Default - @Enumerated(EnumType.STRING) - CardinalStatus status = CardinalStatus.DONE; - - public void update(CardinalUpdateRequest dto) { - this.year = dto.year(); - this.semester = dto.semester(); - } - - public void inProgress() { - this.status = CardinalStatus.IN_PROGRESS; - } - - public void done() { - this.status = CardinalStatus.DONE; - } - -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/SecurityUser.java b/src/main/java/com/weeth/domain/user/domain/entity/SecurityUser.java deleted file mode 100644 index 8ed2a2ee..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/SecurityUser.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.weeth.domain.user.domain.entity; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Collection; -import java.util.List; - -public record SecurityUser( - Long id, - String email, - String name, - String role, - boolean active -) implements UserDetails, Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - public static SecurityUser from(User u) { - return new SecurityUser( - u.getId(), - u.getEmail(), - u.getName(), - u.getRole().name(), - !u.isInactive() - ); - } - - @Override - public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority("ROLE_" + role)); - } - - @Override - public String getPassword() { - return "N/A"; - } - - @Override - public String getUsername() { - return name; - } - - @Override - public boolean isAccountNonExpired() { - return active; - } - - @Override - public boolean isAccountNonLocked() { - return active; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return active; - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/User.java b/src/main/java/com/weeth/domain/user/domain/entity/User.java deleted file mode 100644 index 17a99a31..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/User.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.weeth.domain.user.domain.entity; - -import jakarta.persistence.*; -import com.weeth.domain.attendance.domain.entity.Attendance; -import com.weeth.domain.board.domain.entity.enums.Part; -import com.weeth.domain.user.domain.entity.enums.Department; -import com.weeth.domain.user.domain.entity.enums.Position; -import com.weeth.domain.user.domain.entity.enums.Role; -import com.weeth.domain.user.domain.entity.enums.Status; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.ArrayList; -import java.util.List; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.Update; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "users") -@AllArgsConstructor -@SuperBuilder -public class User extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id") - private Long id; - - @Column(unique = true) - private Long kakaoId; - - @Column(unique = true) - private String appleId; - - private String name; - - private String email; - - private String password; - - private String studentId; - - private String tel; - - @Enumerated(EnumType.STRING) - private Position position; - - @Enumerated(EnumType.STRING) - private Department department; - - @Enumerated(EnumType.STRING) - private Status status; - - @Enumerated(EnumType.STRING) - private Role role; - - private Integer attendanceCount; - - private Integer absenceCount; - - private Integer attendanceRate; - - private Integer penaltyCount; - - private Integer warningCount; - - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List attendances = new ArrayList<>(); - - @PrePersist - public void init() { - status = Status.WAITING; - role = Role.USER; - attendanceCount = 0; - absenceCount = 0; - attendanceRate = 0; - penaltyCount = 0; - warningCount = 0; - } - - public void addKakaoId(long kakaoId) { - this.kakaoId = kakaoId; - } - - public void addAppleId(String appleId) { - this.appleId = appleId; - } - - public void leave() { - this.status = Status.LEFT; - } - - /* - todo 차후 일반 로그인 비활성화시 해당 메서드에서 예외를 날리도록 수정 - */ - public boolean isInactive() { - return this.status != Status.ACTIVE; - } - - public void update(Update dto) { - this.name = dto.name(); - this.email = dto.email(); - this.studentId = dto.studentId(); - this.tel = dto.tel(); - this.department = Department.to(dto.department()); - } - - public void accept() { - this.status = Status.ACTIVE; - } - - public void ban() { - this.status = Status.BANNED; - } - - public void update(String role) { - this.role = Role.valueOf(role); - } - - public void reset(PasswordEncoder passwordEncoder) { - this.password = passwordEncoder.encode(studentId); - } - - public void add(Attendance attendance) { - this.attendances.add(attendance); - } - - public void initAttendance() { - this.attendances.clear(); - this.attendanceCount = 0; - this.absenceCount = 0; - this.attendanceRate = 0; - } - - public void attend() { - attendanceCount++; - calculateRate(); - } - - public void removeAttend() { - if (attendanceCount > 0) { - attendanceCount--; - calculateRate(); - } - } - - public void absent() { - absenceCount++; - calculateRate(); - } - - public void removeAbsent() { - if (absenceCount > 0) { - absenceCount--; - calculateRate(); - } - } - - private void calculateRate() { - if (attendanceCount + absenceCount > 0) { - attendanceRate = (attendanceCount * 100) / (attendanceCount + absenceCount); - } else { - attendanceRate = 0; - } - } - - public void incrementPenaltyCount() { - penaltyCount++; - } - - public void decrementPenaltyCount() { - if (penaltyCount > 0) { - penaltyCount--; - } - } - - public void incrementWarningCount() { - warningCount++; - } - - public void decrementWarningCount() { - if (warningCount > 0) { - warningCount--; - } - } - - public boolean hasRole(Role role) { - return this.role == role; - } - - public Part getUserPart() { - return Part.valueOf(this.position.name()); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/UserCardinal.java b/src/main/java/com/weeth/domain/user/domain/entity/UserCardinal.java deleted file mode 100644 index aca38036..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/UserCardinal.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.weeth.domain.user.domain.entity; - -import jakarta.persistence.*; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@SuperBuilder -public class UserCardinal extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_cardinal_id") - private Long id; - - @ManyToOne - @JoinColumn(name = "user_id") - private User user; - - @ManyToOne - @JoinColumn(name = "cardinal_id") - private Cardinal cardinal; - - public UserCardinal(User user, Cardinal cardinal) { - this.user = user; - this.cardinal = cardinal; - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/CardinalStatus.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/CardinalStatus.java deleted file mode 100644 index 63b20855..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/CardinalStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -public enum CardinalStatus { - IN_PROGRESS, DONE -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/Department.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/Department.java deleted file mode 100644 index f166c7f7..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/Department.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -import com.weeth.domain.user.application.exception.DepartmentNotFoundException; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -@Getter -@RequiredArgsConstructor -public enum Department { - - SW("소프트웨어전공"), - AI("인공지능전공"), - COMPUTER_SCIENCE("컴퓨터공학과"), - INDUSTRIAL_ENGINEERING("산업공학과"), - VISUAL_DESIGN("시각디자인학과"), - BUSINESS("경영학과"), - ECONOMICS("경제학과"), - KOREAN_LANGUAGE("한국어문학과"), - URBAN_PLANNING("도시계획학전공"), - GLOBAL_BUSINESS("글로벌경영학과"), - FINANCIAL_MATHEMATICS("금융수학전공"), - HEALTHCARE_MANAGEMENT("의료산업경영학과"); // 더 필요한 학과는 추후 추가할 예정 - - private final String value; - - public static Department to(String before) { - return Arrays.stream(Department.values()) - .filter(department -> department.getValue().equals(before)) - .findAny() - .orElseThrow(DepartmentNotFoundException::new); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/LoginStatus.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/LoginStatus.java deleted file mode 100644 index b036d17e..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/LoginStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -public enum LoginStatus { - LOGIN, REGISTER, INTEGRATE -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/Position.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/Position.java deleted file mode 100644 index b9a391f6..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/Position.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -public enum Position { - D, - FE, - BE, - PM -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/Role.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/Role.java deleted file mode 100644 index c32bad1d..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/Role.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum Role { - USER, - ADMIN -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/Status.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/Status.java deleted file mode 100644 index 5950a5c7..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/Status.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -public enum Status { - WAITING, - ACTIVE, - BANNED, - LEFT -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/StatusPriority.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/StatusPriority.java deleted file mode 100644 index becc6b02..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/StatusPriority.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -import com.weeth.domain.user.application.exception.StatusNotFoundException; -import lombok.Getter; - -@Getter -public enum StatusPriority { - ACTIVE(1), - WAITING(2), - LEFT(3), - BANNED(4); - - private final int priority; - - StatusPriority(int priority) { - this.priority = priority; - } - - public static StatusPriority fromStatus(Status status) { - if (status == null) { - throw new StatusNotFoundException(); - } - return StatusPriority.valueOf(status.name()); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/entity/enums/UsersOrderBy.java b/src/main/java/com/weeth/domain/user/domain/entity/enums/UsersOrderBy.java deleted file mode 100644 index 83b1d17f..00000000 --- a/src/main/java/com/weeth/domain/user/domain/entity/enums/UsersOrderBy.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.weeth.domain.user.domain.entity.enums; - -public enum UsersOrderBy { - NAME_ASCENDING, // 이름순 정렬 - CARDINAL_DESCENDING; // 기수 기준으로 내림차순 정렬 -} diff --git a/src/main/java/com/weeth/domain/user/domain/repository/CardinalRepository.java b/src/main/java/com/weeth/domain/user/domain/repository/CardinalRepository.java deleted file mode 100644 index 47a29af5..00000000 --- a/src/main/java/com/weeth/domain/user/domain/repository/CardinalRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.weeth.domain.user.domain.repository; - -import java.util.List; -import java.util.Optional; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.enums.CardinalStatus; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CardinalRepository extends JpaRepository { - - Optional findByCardinalNumber(Integer cardinal); - - Optional findByYearAndSemester(Integer year, Integer semester); - - List findAllByStatus(CardinalStatus cardinalStatus); - - Cardinal findFirstByStatusOrderByCardinalNumberDesc(CardinalStatus status); - - List findAllByOrderByCardinalNumberAsc(); - - List findAllByOrderByCardinalNumberDesc(); -} diff --git a/src/main/java/com/weeth/domain/user/domain/repository/UserCardinalRepository.java b/src/main/java/com/weeth/domain/user/domain/repository/UserCardinalRepository.java deleted file mode 100644 index 98024774..00000000 --- a/src/main/java/com/weeth/domain/user/domain/repository/UserCardinalRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.weeth.domain.user.domain.repository; - -import java.util.List; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface UserCardinalRepository extends JpaRepository { - - List findAllByUserOrderByCardinalCardinalNumberDesc(User user); - - @Query("SELECT uc FROM UserCardinal uc WHERE uc.user IN :users ORDER BY uc.user.id, uc.cardinal.cardinalNumber DESC") - List findAllByUsers(List users); - - List findAllByOrderByUser_NameAsc(); - - @Query(""" - select uc.cardinal.cardinalNumber - from UserCardinal uc - where uc.user = :user - order by uc.cardinal.cardinalNumber desc - """) - List findCardinalNumbersByUser(@Param("user") User user); -} diff --git a/src/main/java/com/weeth/domain/user/domain/repository/UserRepository.java b/src/main/java/com/weeth/domain/user/domain/repository/UserRepository.java deleted file mode 100644 index 586ce952..00000000 --- a/src/main/java/com/weeth/domain/user/domain/repository/UserRepository.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.weeth.domain.user.domain.repository; - -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.enums.Status; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import jakarta.persistence.LockModeType; -import jakarta.persistence.QueryHint; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryHints; -import org.springframework.data.repository.query.Param; - -import java.util.List; -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @QueryHints(@QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")) - @Query("SELECT u FROM User u WHERE u.id = :id") - Optional findByIdWithLock(@Param("id") Long id); - - Optional findByEmail(String email); - - Optional findByKakaoId(long kakaoId); - - Optional findByAppleId(String appleId); - - ListfindAllByNameContainingAndStatus(String name, Status status); - - boolean existsByEmail(String email); - - boolean existsByStudentId(String studentId); - - boolean existsByTel(String tel); - - boolean existsByStudentIdAndIdIsNot(String studentId, Long id); - - boolean existsByTelAndIdIsNot(String tel, Long id); - - List findAllByStatusOrderByName(Status status); - - List findAllByOrderByNameAsc(); - - @Query("SELECT uc.user FROM UserCardinal uc WHERE uc.cardinal = :cardinal AND uc.user.status = :status") - List findAllByCardinalAndStatus(@Param("cardinal") Cardinal cardinal, @Param("status") Status status); - - /* - todo 차후 리팩토링 - */ - @Query(""" - SELECT u - FROM User u - JOIN UserCardinal uc ON u.id = uc.user.id - JOIN uc.cardinal c - WHERE u.status = :status - GROUP BY u.id - ORDER BY MAX(c.cardinalNumber) DESC, u.name ASC - """) - Slice findAllByStatusOrderedByCardinalAndName(@Param("status") Status status, Pageable pageable); - - @Query(""" - SELECT u FROM User u - JOIN UserCardinal uc ON uc.user.id = u.id - WHERE u.status = :status - AND uc.cardinal = :cardinal - ORDER BY u.name ASC - """) - Slice findAllByCardinalOrderByNameAsc(@Param("status") Status status, @Param("cardinal") Cardinal cardinal, Pageable pageable); -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/CardinalGetService.java b/src/main/java/com/weeth/domain/user/domain/service/CardinalGetService.java deleted file mode 100644 index 14248651..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/CardinalGetService.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import java.util.List; -import com.weeth.domain.user.application.exception.CardinalNotFoundException; -import com.weeth.domain.user.application.exception.DuplicateCardinalException; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.enums.CardinalStatus; -import com.weeth.domain.user.domain.repository.CardinalRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CardinalGetService { - - private final CardinalRepository cardinalRepository; - - public Cardinal findByAdminSide(Integer cardinal) { - return cardinalRepository.findByCardinalNumber(cardinal) - .orElseGet(() -> cardinalRepository.save(Cardinal.builder().cardinalNumber(cardinal).build())); - } - - public Cardinal findByUserSide(Integer cardinal) { - return cardinalRepository.findByCardinalNumber(cardinal) - .orElseThrow(CardinalNotFoundException::new); - } - - public Cardinal find(Integer year, Integer semester) { - return cardinalRepository.findByYearAndSemester(year, semester) - .orElseThrow(CardinalNotFoundException::new); - } - - public Cardinal findById(long cardinalId) { - return cardinalRepository.findById(cardinalId) - .orElseThrow(CardinalNotFoundException::new); - } - - public List findAll() { - return cardinalRepository.findAllByOrderByCardinalNumberAsc(); - } - - public List findAllCardinalNumberDesc() { - return cardinalRepository.findAllByOrderByCardinalNumberDesc(); - } - - public List findInProgress() { - return cardinalRepository.findAllByStatus(CardinalStatus.IN_PROGRESS); - } - - public void validateCardinal(Integer cardinal) { - if (cardinalRepository.findByCardinalNumber(cardinal).isPresent()) { - throw new DuplicateCardinalException(); - } - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/CardinalSaveService.java b/src/main/java/com/weeth/domain/user/domain/service/CardinalSaveService.java deleted file mode 100644 index 2e755d2f..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/CardinalSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.repository.CardinalRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CardinalSaveService { - - private final CardinalRepository cardinalRepository; - - public Cardinal save(Cardinal cardinal) { - return cardinalRepository.save(cardinal); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/UserCardinalGetService.java b/src/main/java/com/weeth/domain/user/domain/service/UserCardinalGetService.java deleted file mode 100644 index 383a3ed3..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/UserCardinalGetService.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import java.util.Comparator; -import java.util.List; -import com.weeth.domain.user.application.exception.CardinalNotFoundException; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.UserCardinal; -import com.weeth.domain.user.domain.repository.UserCardinalRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class UserCardinalGetService { - - private final UserCardinalRepository userCardinalRepository; - - public List getUserCardinals(User user) { - return userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user); - } - - public List findAll() { - return userCardinalRepository.findAllByOrderByUser_NameAsc(); - } - - public List findAll(List users) { - return userCardinalRepository.findAllByUsers(users); - } - - public boolean notContains(User user, Cardinal cardinal) { - return getUserCardinals(user).stream() - .noneMatch(userCardinal -> userCardinal.getCardinal().equals(cardinal)); - } - - public boolean isCurrent(User user, Cardinal cardinal) { - Integer maxCardinalNumber = getUserCardinals(user).stream() - .map(UserCardinal::getCardinal) - .map(Cardinal::getCardinalNumber) - .max(Integer::compareTo) - .orElseThrow(CardinalNotFoundException::new); - - return maxCardinalNumber < cardinal.getCardinalNumber(); - } - - public Cardinal getCurrentCardinal(User user) { - return getUserCardinals(user).stream() - .map(UserCardinal::getCardinal) - .max(Comparator.comparing(Cardinal::getCardinalNumber)) - .orElseThrow(CardinalNotFoundException::new); - } - - public List getCardinalNumbers(User user) { - return userCardinalRepository.findCardinalNumbersByUser(user); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/UserCardinalSaveService.java b/src/main/java/com/weeth/domain/user/domain/service/UserCardinalSaveService.java deleted file mode 100644 index 83a99999..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/UserCardinalSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import com.weeth.domain.user.domain.entity.UserCardinal; -import com.weeth.domain.user.domain.repository.UserCardinalRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class UserCardinalSaveService { - - private final UserCardinalRepository userCardinalRepository; - - public void save(UserCardinal userCardinal) { - userCardinalRepository.save(userCardinal); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/UserDeleteService.java b/src/main/java/com/weeth/domain/user/domain/service/UserDeleteService.java deleted file mode 100644 index 2f5dd11e..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/UserDeleteService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import jakarta.transaction.Transactional; -import com.weeth.domain.user.domain.entity.User; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class UserDeleteService { - - @Transactional - public void leave(User user) { - user.leave(); - } - - @Transactional - public void ban(User user) { - user.ban(); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/UserGetService.java b/src/main/java/com/weeth/domain/user/domain/service/UserGetService.java deleted file mode 100644 index 3e2b2787..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/UserGetService.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.enums.Status; -import com.weeth.domain.user.domain.repository.UserRepository; -import com.weeth.domain.user.application.exception.UserNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class UserGetService { - - private final UserRepository userRepository; - - public User find(Long userId) { - return userRepository.findById(userId) - .orElseThrow(UserNotFoundException::new); - } - - public User find(String email){ - return userRepository.findByEmail(email) - .orElseThrow(UserNotFoundException::new); - } - - public Optional findByKakaoId(long kakaoId){ - return userRepository.findByKakaoId(kakaoId); - } - - public Optional findByAppleId(String appleId){ - return userRepository.findByAppleId(appleId); - } - - public List search(String keyword) { - return userRepository.findAllByNameContainingAndStatus(keyword, Status.ACTIVE); - } - - public Boolean check(String email) { - return !userRepository.existsByEmail(email); - } - - public List findAll(List userId) { - return userRepository.findAllById(userId); - } - - public List findAllByCardinal(Cardinal cardinal) { - return userRepository.findAllByCardinalAndStatus(cardinal, Status.ACTIVE); - } - - public Slice findAll(Pageable pageable) { - Slice users = userRepository.findAllByStatusOrderedByCardinalAndName(Status.ACTIVE, pageable); - - if (users.isEmpty()) { - throw new UserNotFoundException(); - } - - return users; - } - - public Slice findAll(Pageable pageable, Cardinal cardinal) { - Slice users = userRepository.findAllByCardinalOrderByNameAsc(Status.ACTIVE, cardinal, pageable); - - if (users.isEmpty()) { - throw new UserNotFoundException(); - } - - return users; - } - - public boolean validateStudentId(String studentId) { - return userRepository.existsByStudentId(studentId); - } - - public boolean validateStudentId(String studentId, Long userId) { - return userRepository.existsByStudentIdAndIdIsNot(studentId, userId); - } - - public boolean validateTel(String tel) { - return userRepository.existsByTel(tel); - } - - public boolean validateTel(String tel, Long userId) { - return userRepository.existsByTelAndIdIsNot(tel, userId); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/UserSaveService.java b/src/main/java/com/weeth/domain/user/domain/service/UserSaveService.java deleted file mode 100644 index 7afd4987..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/UserSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class UserSaveService { - - private final UserRepository userRepository; - - public void save(User user) { - userRepository.save(user); - } -} diff --git a/src/main/java/com/weeth/domain/user/domain/service/UserUpdateService.java b/src/main/java/com/weeth/domain/user/domain/service/UserUpdateService.java deleted file mode 100644 index 2f190ed6..00000000 --- a/src/main/java/com/weeth/domain/user/domain/service/UserUpdateService.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.weeth.domain.user.domain.service; - -import jakarta.transaction.Transactional; -import com.weeth.domain.user.domain.entity.User; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.Update; - - -@Service -@Transactional -@RequiredArgsConstructor -public class UserUpdateService { - - public void update(User user, Update dto) { - user.update(dto); - } - - public void accept(User user) { - user.accept(); - } - - public void update(User user, String role) { - user.update(role); - } - - public void reset(User user, PasswordEncoder passwordEncoder) { - user.reset(passwordEncoder); - } -} diff --git a/src/main/java/com/weeth/domain/user/presentation/CardinalController.java b/src/main/java/com/weeth/domain/user/presentation/CardinalController.java deleted file mode 100644 index 17b76427..00000000 --- a/src/main/java/com/weeth/domain/user/presentation/CardinalController.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.weeth.domain.user.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import com.weeth.domain.user.application.dto.request.CardinalSaveRequest; -import com.weeth.domain.user.application.dto.request.CardinalUpdateRequest; -import com.weeth.domain.user.application.dto.response.CardinalResponse; -import com.weeth.domain.user.application.exception.UserErrorCode; -import com.weeth.domain.user.application.usecase.CardinalUseCase; -import com.weeth.global.auth.jwt.application.exception.JwtErrorCode; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static com.weeth.domain.user.presentation.UserResponseCode.*; - -@Tag(name = "CARDINAL") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1") -@ApiErrorCodeExample({UserErrorCode.class, JwtErrorCode.class}) -public class CardinalController { - - private final CardinalUseCase cardinalUseCase; - - @GetMapping("/cardinals") - @Operation(summary = "현재 저장된 기수 목록 조회 API") - public CommonResponse> findAllCardinals() { - List response = cardinalUseCase.findAll(); - - return CommonResponse.success(CARDINAL_FIND_ALL_SUCCESS, response); - } - - @PatchMapping("/admin/cardinals") - @Operation(summary = "[admin] 기수 정보 수정 API") - public CommonResponse updateCardinals(@RequestBody CardinalUpdateRequest dto) { - cardinalUseCase.update(dto); - - return CommonResponse.success(CARDINAL_UPDATE_SUCCESS); - } - - @PostMapping("/admin/cardinals") - @Operation(summary = "[admin] 새로운 기수 정보 저장 API") - public CommonResponse save(@RequestBody @Valid CardinalSaveRequest dto) { - cardinalUseCase.save(dto); - - return CommonResponse.success(CARDINAL_SAVE_SUCCESS); - } - -} diff --git a/src/main/java/com/weeth/domain/user/presentation/UserAdminController.java b/src/main/java/com/weeth/domain/user/presentation/UserAdminController.java deleted file mode 100644 index e91dcc01..00000000 --- a/src/main/java/com/weeth/domain/user/presentation/UserAdminController.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.weeth.domain.user.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.user.application.exception.UserErrorCode; -import com.weeth.domain.user.application.usecase.UserManageUseCase; -import com.weeth.domain.user.domain.entity.enums.UsersOrderBy; -import com.weeth.global.auth.jwt.application.exception.JwtErrorCode; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.*; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.AdminResponse; -import static com.weeth.domain.user.presentation.UserResponseCode.*; - -@Tag(name = "USER ADMIN", description = "[ADMIN] 사용자 어드민 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/admin/users") -@ApiErrorCodeExample({UserErrorCode.class, JwtErrorCode.class}) -public class UserAdminController { - - private final UserManageUseCase userManageUseCase; - - @GetMapping("/all") - @Operation(summary = "어드민용 회원 조회") - public CommonResponse> findAll(@RequestParam UsersOrderBy orderBy) { - return CommonResponse.success(USER_FIND_ALL_SUCCESS, userManageUseCase.findAllByAdmin(orderBy)); - } - - @PatchMapping - @Operation(summary = "가입 신청 승인") - public CommonResponse accept(@RequestBody UserId userId) { - userManageUseCase.accept(userId); - return CommonResponse.success(USER_ACCEPT_SUCCESS); - } - - @DeleteMapping - @Operation(summary = "유저 추방") - public CommonResponse ban(@RequestBody UserId userId) { - userManageUseCase.ban(userId); - return CommonResponse.success(USER_BAN_SUCCESS); - } - - @PatchMapping("/role") - @Operation(summary = "관리자로 승격/강등") - public CommonResponse update(@RequestBody List request) { - userManageUseCase.update(request); - return CommonResponse.success(USER_ROLE_UPDATE_SUCCESS); - } - - @PatchMapping("/apply") - @Operation(summary = "다음 기수도 이어서 진행") - public CommonResponse applyOB(@RequestBody List request) { - userManageUseCase.applyOB(request); - return CommonResponse.success(USER_APPLY_OB_SUCCESS); - } - - @PatchMapping("/reset") - @Operation(summary = "회원 비밀번호 초기화") - public CommonResponse resetPassword(@RequestBody UserId userId) { - userManageUseCase.reset(userId); - return CommonResponse.success(USER_PASSWORD_RESET_SUCCESS); - } -} diff --git a/src/main/java/com/weeth/domain/user/presentation/UserController.java b/src/main/java/com/weeth/domain/user/presentation/UserController.java deleted file mode 100644 index 49ed0767..00000000 --- a/src/main/java/com/weeth/domain/user/presentation/UserController.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.weeth.domain.user.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import com.weeth.domain.user.application.dto.response.UserResponseDto; -import com.weeth.domain.user.application.dto.response.UserResponseDto.SummaryResponse; -import com.weeth.domain.user.application.dto.response.UserResponseDto.UserResponse; -import com.weeth.domain.user.application.exception.UserErrorCode; -import com.weeth.domain.user.application.usecase.UserManageUseCase; -import com.weeth.domain.user.application.usecase.UserUseCase; -import com.weeth.domain.user.domain.service.UserGetService; -import com.weeth.global.auth.annotation.CurrentUser; -import com.weeth.global.auth.jwt.application.dto.JwtDto; -import com.weeth.global.auth.jwt.application.exception.JwtErrorCode; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static com.weeth.domain.user.application.dto.request.UserRequestDto.*; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.Response; -import static com.weeth.domain.user.application.dto.response.UserResponseDto.SocialLoginResponse; -import static com.weeth.domain.user.presentation.UserResponseCode.*; - -@Tag(name = "USER", description = "사용자 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/users") -@ApiErrorCodeExample({UserErrorCode.class, JwtErrorCode.class}) -public class UserController { - - private final UserUseCase userUseCase; - private final UserManageUseCase userManageUseCase; - private final UserGetService userGetService; - - @PostMapping("/kakao/login") - @Operation(summary = "카카오 소셜 로그인 API") - public CommonResponse login(@RequestBody @Valid Login dto) { - SocialLoginResponse response = userUseCase.login(dto); - return CommonResponse.success(SOCIAL_LOGIN_SUCCESS, response); - } - - @PostMapping("/kakao/auth") - @Operation(summary = "카카오 소셜 회원가입 전 요청 API (미사용 API)") - public CommonResponse beforeRegister(@RequestBody @Valid Login dto) { - UserResponseDto.SocialAuthResponse response = userUseCase.authenticate(dto); - return CommonResponse.success(SOCIAL_AUTH_SUCCESS, response); - } - - @PostMapping("/apply") - @Operation(summary = "동아리 지원 신청. 현재 사용하지 않으므로 회원가입 시 /kakao/register api로 요청 바람") - public CommonResponse apply(@RequestBody @Valid SignUp dto) { - userUseCase.apply(dto); - return CommonResponse.success(USER_APPLY_SUCCESS); - } - - @PostMapping("/kakao/register") - @Operation(summary = "소셜 회원가입") - public CommonResponse register(@RequestBody @Valid Register dto) { - userUseCase.socialRegister(dto); - return CommonResponse.success(USER_APPLY_SUCCESS); - } - - @PatchMapping("/kakao/link") - @Operation(summary = "카카오 소셜 로그인 연동") - public CommonResponse integrate(@RequestBody @Valid NormalLogin dto) { - return CommonResponse.success(SOCIAL_INTEGRATE_SUCCESS, userUseCase.integrate(dto)); - } - - @PostMapping("/apple/login") - @Operation(summary = "애플 소셜 로그인 API") - public CommonResponse appleLogin(@RequestBody @Valid Login dto) { - SocialLoginResponse response = userUseCase.appleLogin(dto); - return CommonResponse.success(SOCIAL_LOGIN_SUCCESS, response); - } - - @PostMapping("/apple/register") - @Operation(summary = "애플 소셜 회원가입 (dev 전용 - 바로 ACTIVE)") - public CommonResponse appleRegister(@RequestBody @Valid Register dto) { - userUseCase.appleRegister(dto); - return CommonResponse.success(USER_APPLY_SUCCESS); - } - - @GetMapping("/email") - @Operation(summary = "이메일 중복 확인") - public CommonResponse checkEmail(@RequestParam String email) { - return CommonResponse.success(USER_EMAIL_CHECK_SUCCESS, userGetService.check(email)); - } - - @GetMapping("/all") - @Operation(summary = "동아리 멤버 전체 조회(전체/기수별)") - public CommonResponse> findAllUser(@RequestParam("pageNumber") int pageNumber, - @RequestParam("pageSize") int pageSize, - @RequestParam(required = false) Integer cardinal) { - return CommonResponse.success(USER_FIND_ALL_SUCCESS, userUseCase.findAllUser(pageNumber, pageSize, cardinal)); - } - - @GetMapping("/search") - @Operation(summary = "동아리 멤버 검색") - public CommonResponse> searchUser(@RequestParam String keyword) { - return CommonResponse.success(USER_FIND_BY_ID_SUCCESS, userUseCase.searchUser(keyword)); - } - - @GetMapping("/details") - @Operation(summary = "특정 멤버 상세 조회") - public CommonResponse findUser(@RequestParam Long userId) { - return CommonResponse.success( - USER_DETAILS_SUCCESS, userUseCase.findUserDetails(userId) - ); - } - - @GetMapping - @Operation(summary = "내 정보 조회") - public CommonResponse find(@Parameter(hidden = true) @CurrentUser Long userId) { - return CommonResponse.success(USER_FIND_BY_ID_SUCCESS, userUseCase.find(userId)); - } - - @GetMapping("/info") - @Operation(summary = "전역 내 정보 조회 API") - public CommonResponse findMyInfo(@Parameter(hidden = true) @CurrentUser Long userId) { - return CommonResponse.success(USER_FIND_BY_ID_SUCCESS, userUseCase.findUserInfo(userId)); - } - - @PatchMapping - @Operation(summary = "내 정보 수정") - public CommonResponse update(@RequestBody @Valid Update dto, @Parameter(hidden = true) @CurrentUser Long userId) { - userUseCase.update(dto, userId); - return CommonResponse.success(USER_UPDATE_SUCCESS); - } - - @DeleteMapping - @Operation(summary = "동아리 탈퇴") - public CommonResponse leave(@Parameter(hidden = true) @CurrentUser Long userId) { - userManageUseCase.leave(userId); - return CommonResponse.success(USER_LEAVE_SUCCESS); - } - - @PostMapping("/refresh") - @Operation(summary = "JWT 토큰 재발급 API") - public CommonResponse refresh(@Parameter(hidden = true) @RequestHeader("Authorization_refresh") String refreshToken) { - return CommonResponse.success(JWT_REFRESH_SUCCESS, userUseCase.refresh(refreshToken)); - } -} diff --git a/src/main/java/com/weeth/domain/user/presentation/UserResponseCode.java b/src/main/java/com/weeth/domain/user/presentation/UserResponseCode.java deleted file mode 100644 index 95beccc9..00000000 --- a/src/main/java/com/weeth/domain/user/presentation/UserResponseCode.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.weeth.domain.user.presentation; - -import com.weeth.global.common.response.ResponseCodeInterface; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -public enum UserResponseCode implements ResponseCodeInterface { - // UserAdminController 관련 - USER_FIND_ALL_SUCCESS(1800, HttpStatus.OK, "모든 회원 정보를 성공적으로 조회했습니다."), - USER_DETAILS_SUCCESS(1801, HttpStatus.OK, "특정 회원의 상세 정보를 성공적으로 조회했습니다."), - USER_ACCEPT_SUCCESS(1802, HttpStatus.OK, "회원 가입 승인이 성공적으로 처리되었습니다."), - USER_BAN_SUCCESS(1803, HttpStatus.OK, "회원이 성공적으로 차단되었습니다."), - USER_ROLE_UPDATE_SUCCESS(1804, HttpStatus.OK, "회원의 역할이 성공적으로 수정되었습니다."), - USER_APPLY_OB_SUCCESS(1805, HttpStatus.OK, "OB 신청이 성공적으로 처리되었습니다."), - USER_PASSWORD_RESET_SUCCESS(1806, HttpStatus.OK, "비밀번호가 성공적으로 초기화되었습니다."), - // UserController 관련 - USER_APPLY_SUCCESS(1807, HttpStatus.OK, "회원 가입 신청이 성공적으로 처리되었습니다."), - USER_EMAIL_CHECK_SUCCESS(1808, HttpStatus.OK, "이메일 중복 검사가 성공적으로 처리되었습니다."), - USER_FIND_BY_ID_SUCCESS(1809, HttpStatus.OK, "회원 정보가 성공적으로 조회되었습니다."), - USER_UPDATE_SUCCESS(1810, HttpStatus.OK, "회원 정보가 성공적으로 수정되었습니다."), - USER_LEAVE_SUCCESS(1811, HttpStatus.OK, "회원 탈퇴가 성공적으로 처리되었습니다."), - SOCIAL_LOGIN_SUCCESS(1812, HttpStatus.OK, "소셜 로그인에 성공했습니다."), - SOCIAL_REGISTER_SUCCESS(1813, HttpStatus.OK, "소셜 회원가입에 성공했습니다."), - SOCIAL_AUTH_SUCCESS(1814, HttpStatus.OK, "소셜 인증에 성공했습니다."), - SOCIAL_INTEGRATE_SUCCESS(1815, HttpStatus.OK, "소셜 로그인 연동에 성공했습니다."), - JWT_REFRESH_SUCCESS(1816, HttpStatus.OK, "토큰 재발급에 성공했습니다."), - - // CardinalController 관련 - CARDINAL_FIND_ALL_SUCCESS(1817, HttpStatus.OK, "전체 기수 조회에 성공했습니다."), - CARDINAL_SAVE_SUCCESS(1818, HttpStatus.OK, "기수 저장에 성공했습니다."), - CARDINAL_UPDATE_SUCCESS(1819, HttpStatus.OK, "기수 수정에 성공했습니다."); - - private final int code; - private final HttpStatus status; - private final String message; - - UserResponseCode(int code, HttpStatus status, String message) { - this.code = code; - this.status = status; - this.message = message; - } -} diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt index 70a54fad..e8a79b3e 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt @@ -4,19 +4,22 @@ import com.weeth.domain.account.application.dto.request.AccountSaveRequest import com.weeth.domain.account.application.exception.AccountExistsException import com.weeth.domain.account.domain.entity.Account import com.weeth.domain.account.domain.repository.AccountRepository -import com.weeth.domain.user.domain.service.CardinalGetService +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.repository.CardinalRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class ManageAccountUseCase( private val accountRepository: AccountRepository, - private val cardinalGetService: CardinalGetService, + private val cardinalRepository: CardinalRepository, ) { @Transactional fun save(request: AccountSaveRequest) { if (accountRepository.existsByCardinal(request.cardinal)) throw AccountExistsException() - cardinalGetService.findByAdminSide(request.cardinal) + cardinalRepository.findByCardinalNumber(request.cardinal).orElseGet { + cardinalRepository.save(Cardinal.create(cardinalNumber = request.cardinal)) + } accountRepository.save(Account.create(request.description, request.totalAmount, request.cardinal)) } } diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt index ef0c939f..34c06373 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -13,7 +13,8 @@ import com.weeth.domain.file.application.mapper.FileMapper import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.file.domain.repository.FileRepository -import com.weeth.domain.user.domain.service.CardinalGetService +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.repository.CardinalRepository import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -24,12 +25,18 @@ class ManageReceiptUseCase( private val accountRepository: AccountRepository, private val fileReader: FileReader, private val fileRepository: FileRepository, - private val cardinalGetService: CardinalGetService, + private val cardinalRepository: CardinalRepository, private val fileMapper: FileMapper, ) { + private fun ensureCardinalExists(cardinalNumber: Int) { + cardinalRepository.findByCardinalNumber(cardinalNumber).orElseGet { + cardinalRepository.save(Cardinal.create(cardinalNumber = cardinalNumber)) + } + } + @Transactional fun save(request: ReceiptSaveRequest) { - cardinalGetService.findByAdminSide(request.cardinal) + ensureCardinalExists(request.cardinal) val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() val receipt = receiptRepository.save( @@ -44,7 +51,7 @@ class ManageReceiptUseCase( receiptId: Long, request: ReceiptUpdateRequest, ) { - cardinalGetService.findByAdminSide(request.cardinal) + ensureCardinalExists(request.cardinal) val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() if (receipt.account.id != account.id) throw ReceiptAccountMismatchException() diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt index 434ad7a9..1b72be7a 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt @@ -10,8 +10,6 @@ data class AttendanceInfoResponse( val status: Status?, @field:Schema(description = "사용자 이름", example = "이지훈") val name: String?, - @field:Schema(description = "직책", example = "BE") - val position: String?, @field:Schema(description = "소속 학과", example = "컴퓨터공학과") val department: String?, @field:Schema(description = "학번", example = "20201234") diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt b/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt index a60bc189..8ccf206b 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt @@ -30,9 +30,9 @@ class AttendanceMapper { attendances: List, ): AttendanceDetailResponse = AttendanceDetailResponse( - attendanceCount = user.attendanceCount ?: 0, - total = (user.attendanceCount ?: 0) + (user.absenceCount ?: 0), - absenceCount = user.absenceCount ?: 0, + attendanceCount = user.attendanceCount, + total = user.attendanceCount + user.absenceCount, + absenceCount = user.absenceCount, attendances = attendances, ) @@ -51,8 +51,7 @@ class AttendanceMapper { id = attendance.id, status = attendance.status, name = attendance.user.name, - position = attendance.user.position?.name, - department = attendance.user.department?.name, + department = attendance.user.department, studentId = attendance.user.studentId, ) } diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt index 8f92ae0b..7d0efcd6 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt @@ -4,14 +4,14 @@ import com.weeth.domain.attendance.application.exception.AttendanceCodeMismatchE import com.weeth.domain.attendance.application.exception.AttendanceNotFoundException import com.weeth.domain.attendance.domain.enums.Status import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @Service class CheckInAttendanceUseCase( - private val userGetService: UserGetService, + private val userReader: UserReader, private val attendanceRepository: AttendanceRepository, ) { @Transactional @@ -19,7 +19,7 @@ class CheckInAttendanceUseCase( userId: Long, code: Int, ) { - val user = userGetService.find(userId) + val user = userReader.getById(userId) val now = LocalDateTime.now() val todayAttendance = diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt index 6a8c63cd..59a39143 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt @@ -8,8 +8,8 @@ import com.weeth.domain.attendance.domain.repository.AttendanceRepository import com.weeth.domain.schedule.domain.service.MeetingGetService import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.domain.entity.enums.Status -import com.weeth.domain.user.domain.service.UserCardinalGetService -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.service.UserCardinalPolicy import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -17,14 +17,14 @@ import java.time.LocalDate @Service @Transactional(readOnly = true) class GetAttendanceQueryService( - private val userGetService: UserGetService, - private val userCardinalGetService: UserCardinalGetService, + private val userReader: UserReader, + private val userCardinalPolicy: UserCardinalPolicy, private val meetingGetService: MeetingGetService, private val attendanceRepository: AttendanceRepository, private val mapper: AttendanceMapper, ) { fun findAttendance(userId: Long): AttendanceSummaryResponse { - val user = userGetService.find(userId) + val user = userReader.getById(userId) val today = LocalDate.now() val todayAttendance = @@ -38,8 +38,8 @@ class GetAttendanceQueryService( } fun findAllDetailsByCurrentCardinal(userId: Long): AttendanceDetailResponse { - val user = userGetService.find(userId) - val currentCardinal = userCardinalGetService.getCurrentCardinal(user) + val user = userReader.getById(userId) + val currentCardinal = userCardinalPolicy.getCurrentCardinal(user) val responses = attendanceRepository diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt index f19106dc..f44045c4 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt @@ -15,8 +15,7 @@ class AttendanceSaveService( meetings: List?, ) { meetings?.forEach { meeting -> - val attendance = attendanceRepository.save(Attendance(meeting, user)) - user.add(attendance) + attendanceRepository.save(Attendance(meeting, user)) } } diff --git a/src/main/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCase.kt b/src/main/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCase.kt index afe3bf21..d74c52a6 100644 --- a/src/main/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCase.kt @@ -18,7 +18,7 @@ import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.file.domain.repository.FileRepository import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -26,7 +26,7 @@ import org.springframework.transaction.annotation.Transactional class ManagePostUseCase( private val postRepository: PostRepository, private val boardRepository: BoardRepository, // 동일 도메인 - private val userGetService: UserGetService, + private val userReader: UserReader, private val fileRepository: FileRepository, private val fileReader: FileReader, private val fileMapper: FileMapper, @@ -38,7 +38,7 @@ class ManagePostUseCase( request: CreatePostRequest, userId: Long, ): PostSaveResponse { - val user = userGetService.find(userId) // todo: Reader 인터페이스로 수정 + val user = userReader.getById(userId) val board = findBoard(boardId) checkWritePermission(board, user) diff --git a/src/main/kotlin/com/weeth/domain/comment/application/dto/response/CommentResponse.kt b/src/main/kotlin/com/weeth/domain/comment/application/dto/response/CommentResponse.kt index 810996c0..b668405a 100644 --- a/src/main/kotlin/com/weeth/domain/comment/application/dto/response/CommentResponse.kt +++ b/src/main/kotlin/com/weeth/domain/comment/application/dto/response/CommentResponse.kt @@ -1,7 +1,6 @@ package com.weeth.domain.comment.application.dto.response import com.weeth.domain.file.application.dto.response.FileResponse -import com.weeth.domain.user.domain.entity.enums.Position import com.weeth.domain.user.domain.entity.enums.Role import io.swagger.v3.oas.annotations.media.Schema import java.time.LocalDateTime @@ -11,8 +10,6 @@ data class CommentResponse( val id: Long, @field:Schema(description = "작성자 이름", example = "홍길동") val name: String, - @field:Schema(description = "작성자 포지션", example = "BE") - val position: Position, @field:Schema(description = "작성자 역할", example = "USER") val role: Role, @field:Schema(description = "댓글 내용", example = "댓글입니다.") diff --git a/src/main/kotlin/com/weeth/domain/comment/application/mapper/CommentMapper.kt b/src/main/kotlin/com/weeth/domain/comment/application/mapper/CommentMapper.kt index 74007626..80f41d93 100644 --- a/src/main/kotlin/com/weeth/domain/comment/application/mapper/CommentMapper.kt +++ b/src/main/kotlin/com/weeth/domain/comment/application/mapper/CommentMapper.kt @@ -15,7 +15,6 @@ class CommentMapper { CommentResponse( id = comment.id, name = comment.user.name, - position = comment.user.position, role = comment.user.role, content = comment.content, time = comment.modifiedAt, diff --git a/src/main/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCase.kt b/src/main/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCase.kt index af074fdb..da0409fe 100644 --- a/src/main/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCase.kt @@ -15,7 +15,7 @@ import com.weeth.domain.file.application.mapper.FileMapper import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.file.domain.repository.FileRepository -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -23,7 +23,7 @@ import org.springframework.transaction.annotation.Transactional class ManageCommentUseCase( private val commentRepository: CommentRepository, private val postRepository: PostRepository, - private val userGetService: UserGetService, + private val userReader: UserReader, private val fileReader: FileReader, private val fileRepository: FileRepository, private val fileMapper: FileMapper, @@ -34,7 +34,7 @@ class ManageCommentUseCase( postId: Long, userId: Long, ) { - val user = userGetService.find(userId) + val user = userReader.getById(userId) val post = findPostWithLock(postId) val parent = dto.parentCommentId?.let { parentId -> diff --git a/src/main/kotlin/com/weeth/domain/penalty/application/usecase/command/SavePenaltyUseCase.kt b/src/main/kotlin/com/weeth/domain/penalty/application/usecase/command/SavePenaltyUseCase.kt index b9391d0a..b3a96c19 100644 --- a/src/main/kotlin/com/weeth/domain/penalty/application/usecase/command/SavePenaltyUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/penalty/application/usecase/command/SavePenaltyUseCase.kt @@ -6,7 +6,7 @@ import com.weeth.domain.penalty.domain.enums.PenaltyType import com.weeth.domain.penalty.domain.repository.PenaltyRepository import com.weeth.domain.user.application.exception.UserNotFoundException import com.weeth.domain.user.domain.repository.UserRepository -import com.weeth.domain.user.domain.service.UserCardinalGetService +import com.weeth.domain.user.domain.service.UserCardinalPolicy import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -14,7 +14,7 @@ import org.springframework.transaction.annotation.Transactional class SavePenaltyUseCase( private val penaltyRepository: PenaltyRepository, private val userRepository: UserRepository, - private val userCardinalGetService: UserCardinalGetService, + private val userCardinalPolicy: UserCardinalPolicy, private val mapper: PenaltyMapper, ) { companion object { @@ -27,7 +27,7 @@ class SavePenaltyUseCase( userRepository .findByIdWithLock(request.userId) .orElseThrow { UserNotFoundException() } - val cardinal = userCardinalGetService.getCurrentCardinal(user) + val cardinal = userCardinalPolicy.getCurrentCardinal(user) val penalty = mapper.toEntity(request, user, cardinal) penaltyRepository.save(penalty) diff --git a/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyQueryService.kt b/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyQueryService.kt index cb842e2f..d0321f8e 100644 --- a/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyQueryService.kt @@ -4,9 +4,10 @@ import com.weeth.domain.penalty.application.dto.response.PenaltyByCardinalRespon import com.weeth.domain.penalty.application.dto.response.PenaltyResponse import com.weeth.domain.penalty.application.mapper.PenaltyMapper import com.weeth.domain.penalty.domain.repository.PenaltyRepository -import com.weeth.domain.user.domain.service.CardinalGetService -import com.weeth.domain.user.domain.service.UserCardinalGetService -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalReader +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.service.UserCardinalPolicy import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -14,25 +15,26 @@ import org.springframework.transaction.annotation.Transactional @Transactional(readOnly = true) class GetPenaltyQueryService( private val penaltyRepository: PenaltyRepository, - private val userGetService: UserGetService, - private val userCardinalGetService: UserCardinalGetService, - private val cardinalGetService: CardinalGetService, + private val userReader: UserReader, + private val userCardinalReader: UserCardinalReader, + private val userCardinalPolicy: UserCardinalPolicy, + private val cardinalReader: CardinalReader, private val mapper: PenaltyMapper, ) { fun findAllByCardinal(cardinalNumber: Int?): List { val cardinals = if (cardinalNumber == null) { - cardinalGetService.findAllCardinalNumberDesc() + cardinalReader.findAllByCardinalNumberDesc() } else { - listOf(cardinalGetService.findByAdminSide(cardinalNumber)) + listOf(cardinalReader.getByCardinalNumber(cardinalNumber)) } return cardinals.map { cardinal -> val penalties = penaltyRepository.findByCardinalIdOrderByIdDesc(cardinal.id) val users = penalties.map { it.user }.distinct() val userCardinalsMap = - userCardinalGetService - .findAll(users) + userCardinalReader + .findAllByUsersOrderByCardinalDesc(users) .groupBy { it.user.id } val responses = @@ -49,10 +51,10 @@ class GetPenaltyQueryService( } fun findByUser(userId: Long): PenaltyResponse { - val user = userGetService.find(userId) - val currentCardinal = userCardinalGetService.getCurrentCardinal(user) + val user = userReader.getById(userId) + val currentCardinal = userCardinalPolicy.getCurrentCardinal(user) val penalties = penaltyRepository.findByUserIdAndCardinalIdOrderByIdDesc(userId, currentCardinal.id) - val userCardinals = userCardinalGetService.getUserCardinals(user) + val userCardinals = userCardinalReader.findAllByUser(user) return mapper.toResponse(user, penalties, userCardinals) } diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/CardinalSaveRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/CardinalSaveRequest.kt new file mode 100644 index 00000000..2ebe2154 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/CardinalSaveRequest.kt @@ -0,0 +1,19 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull + +data class CardinalSaveRequest( + @field:NotNull + @field:Schema(description = "기수", example = "4") + val cardinalNumber: Int, + @field:NotNull + @field:Schema(description = "년도", example = "2024") + val year: Int, + @field:NotNull + @field:Schema(description = "학기", example = "2") + val semester: Int, + @field:NotNull + @field:Schema(description = "현재 진행중 여부", example = "false") + val inProgress: Boolean, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/CardinalUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/CardinalUpdateRequest.kt new file mode 100644 index 00000000..2e3eda0a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/CardinalUpdateRequest.kt @@ -0,0 +1,19 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull + +data class CardinalUpdateRequest( + @field:NotNull + @field:Schema(description = "기수 ID", example = "1") + val id: Long, + @field:NotNull + @field:Schema(description = "년도", example = "2024") + val year: Int, + @field:NotNull + @field:Schema(description = "학기", example = "2") + val semester: Int, + @field:NotNull + @field:Schema(description = "현재 진행중 여부", example = "false") + val inProgress: Boolean, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/SignUpRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/SignUpRequest.kt new file mode 100644 index 00000000..ec4a464f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/SignUpRequest.kt @@ -0,0 +1,28 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull + +data class SignUpRequest( + @field:Schema(description = "이름", example = "홍길동") + @field:NotBlank + val name: String, + @field:Schema(description = "이메일", example = "hong@example.com") + @field:Email + @field:NotBlank + val email: String, + @field:Schema(description = "학번", example = "20201234") + @field:NotBlank + val studentId: String, + @field:Schema(description = "전화번호", example = "01012345678") + @field:NotBlank + val tel: String, + @field:Schema(description = "학과", example = "컴퓨터공학과") + @field:NotNull + val department: String, + @field:Schema(description = "지원 기수", example = "7") + @field:NotNull + val cardinal: Int, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/SocialLoginRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/SocialLoginRequest.kt new file mode 100644 index 00000000..df1e5772 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/SocialLoginRequest.kt @@ -0,0 +1,18 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank + +data class SocialLoginRequest( + @field:Schema(description = "OAuth2 인가 코드(auth code)", example = "SplxlOBeZQQYbYS6WxSbIA") + @field:NotBlank + val authCode: String, + @field:Schema(description = "추가 입력 이름(선택)", example = "홍길동", nullable = true) + val name: String? = null, + @field:Schema(description = "추가 입력 학번(선택)", example = "20201234", nullable = true) + val studentId: String? = null, + @field:Schema(description = "추가 입력 전화번호(선택)", example = "01012345678", nullable = true) + val tel: String? = null, + @field:Schema(description = "추가 입력 학과(선택)", example = "컴퓨터공학과", nullable = true) + val department: String? = null, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/UpdateUserProfileRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UpdateUserProfileRequest.kt new file mode 100644 index 00000000..ed67dbfb --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UpdateUserProfileRequest.kt @@ -0,0 +1,25 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull + +data class UpdateUserProfileRequest( + @field:Schema(description = "이름", example = "홍길동") + @field:NotBlank + val name: String, + @field:Schema(description = "이메일", example = "hong@example.com") + @field:Email + @field:NotBlank + val email: String, + @field:Schema(description = "학번", example = "20201234") + @field:NotBlank + val studentId: String, + @field:Schema(description = "전화번호", example = "01012345678") + @field:NotBlank + val tel: String, + @field:Schema(description = "학과", example = "컴퓨터공학과") + @field:NotNull + val department: String, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserApplyObRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserApplyObRequest.kt new file mode 100644 index 00000000..214c87e6 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserApplyObRequest.kt @@ -0,0 +1,13 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull + +data class UserApplyObRequest( + @field:Schema(description = "대상 사용자 ID", example = "1") + @field:NotNull + val userId: Long, + @field:Schema(description = "적용할 기수", example = "8") + @field:NotNull + val cardinal: Int, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserIdsRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserIdsRequest.kt new file mode 100644 index 00000000..7b05934a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserIdsRequest.kt @@ -0,0 +1,12 @@ +package com.weeth.domain.user.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull + +data class UserIdsRequest( + @field:Schema(description = "처리 대상 사용자 ID 목록", example = "[1, 2, 3]") + @field:NotNull + @field:NotEmpty + val userId: List, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserRoleUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserRoleUpdateRequest.kt new file mode 100644 index 00000000..9cd0c4a1 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/request/UserRoleUpdateRequest.kt @@ -0,0 +1,14 @@ +package com.weeth.domain.user.application.dto.request + +import com.weeth.domain.user.domain.entity.enums.Role +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull + +data class UserRoleUpdateRequest( + @field:Schema(description = "대상 사용자 ID", example = "1") + @field:NotNull + val userId: Long, + @field:Schema(description = "변경할 권한", example = "ADMIN") + @field:NotNull + val role: Role, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/AdminUserResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/AdminUserResponse.kt new file mode 100644 index 00000000..1077baef --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/AdminUserResponse.kt @@ -0,0 +1,41 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.user.domain.entity.enums.Role +import com.weeth.domain.user.domain.entity.enums.Status +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class AdminUserResponse( + @field:Schema(description = "사용자 ID", example = "1") + val id: Long, + @field:Schema(description = "이름", example = "홍길동") + val name: String, + @field:Schema(description = "이메일", example = "hong@example.com") + val email: String, + @field:Schema(description = "학번", example = "20201234") + val studentId: String, + @field:Schema(description = "전화번호", example = "01012345678") + val tel: String, + @field:Schema(description = "학과", example = "컴퓨터공학과") + val department: String, + @field:Schema(description = "소속 기수 목록", example = "[6, 7]") + val cardinals: List, + @field:Schema(description = "회원 상태", example = "ACTIVE") + val status: Status, + @field:Schema(description = "권한", example = "USER", nullable = true) + val role: Role?, + @field:Schema(description = "출석 횟수", example = "8") + val attendanceCount: Int, + @field:Schema(description = "결석 횟수", example = "2") + val absenceCount: Int, + @field:Schema(description = "출석률", example = "80") + val attendanceRate: Int, + @field:Schema(description = "패널티 횟수", example = "1") + val penaltyCount: Int, + @field:Schema(description = "경고 횟수", example = "0") + val warningCount: Int, + @field:Schema(description = "생성 시각") + val createdAt: LocalDateTime?, + @field:Schema(description = "수정 시각") + val modifiedAt: LocalDateTime?, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/CardinalResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/CardinalResponse.kt new file mode 100644 index 00000000..00fd08d8 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/CardinalResponse.kt @@ -0,0 +1,22 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.user.domain.entity.enums.CardinalStatus +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class CardinalResponse( + @field:Schema(description = "기수 ID", example = "1") + val id: Long, + @field:Schema(description = "기수 번호", example = "7") + val cardinalNumber: Int, + @field:Schema(description = "년도", example = "2025", nullable = true) + val year: Int?, + @field:Schema(description = "학기", example = "1", nullable = true) + val semester: Int?, + @field:Schema(description = "기수 상태", example = "CURRENT") + val status: CardinalStatus, + @field:Schema(description = "생성 시각") + val createdAt: LocalDateTime?, + @field:Schema(description = "수정 시각") + val modifiedAt: LocalDateTime?, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/SocialLoginResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/SocialLoginResponse.kt new file mode 100644 index 00000000..3ad05ed4 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/SocialLoginResponse.kt @@ -0,0 +1,16 @@ +package com.weeth.domain.user.application.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class SocialLoginResponse( + @field:Schema(description = "로그인 사용자 이메일", example = "hong@example.com") + val email: String, + @field:Schema(description = "액세스 토큰") + val accessToken: String, + @field:Schema(description = "리프레시 토큰") + val refreshToken: String, + @field:Schema(description = "신규 회원 여부", example = "true") + val isNewUser: Boolean, + @field:Schema(description = "프로필 완성 여부", example = "false") + val profileCompleted: Boolean, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserDetailsResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserDetailsResponse.kt new file mode 100644 index 00000000..4d32406e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserDetailsResponse.kt @@ -0,0 +1,21 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.user.domain.entity.enums.Role +import io.swagger.v3.oas.annotations.media.Schema + +data class UserDetailsResponse( + @field:Schema(description = "사용자 ID", example = "1") + val id: Long, + @field:Schema(description = "이름", example = "홍길동") + val name: String, + @field:Schema(description = "이메일", example = "hong@example.com") + val email: String, + @field:Schema(description = "학번", example = "20201234") + val studentId: String, + @field:Schema(description = "학과", example = "컴퓨터공학과") + val department: String, + @field:Schema(description = "소속 기수 목록", example = "[6, 7]") + val cardinals: List, + @field:Schema(description = "권한", example = "USER", nullable = true) + val role: Role?, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserInfoResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserInfoResponse.kt new file mode 100644 index 00000000..f3f2d009 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserInfoResponse.kt @@ -0,0 +1,15 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.user.domain.entity.enums.Role +import io.swagger.v3.oas.annotations.media.Schema + +data class UserInfoResponse( + @field:Schema(description = "사용자 ID", example = "1") + val id: Long, + @field:Schema(description = "이름", example = "홍길동") + val name: String, + @field:Schema(description = "소속 기수 목록", example = "[6, 7]") + val cardinals: List, + @field:Schema(description = "권한", example = "USER", nullable = true) + val role: Role?, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserProfileResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserProfileResponse.kt new file mode 100644 index 00000000..61599f61 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserProfileResponse.kt @@ -0,0 +1,23 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.user.domain.entity.enums.Role +import io.swagger.v3.oas.annotations.media.Schema + +data class UserProfileResponse( + @field:Schema(description = "사용자 ID", example = "1") + val id: Long, + @field:Schema(description = "이름", example = "홍길동") + val name: String, + @field:Schema(description = "이메일", example = "hong@example.com") + val email: String, + @field:Schema(description = "학번", example = "20201234") + val studentId: String, + @field:Schema(description = "전화번호", example = "01012345678") + val tel: String, + @field:Schema(description = "학과", example = "컴퓨터공학과") + val department: String, + @field:Schema(description = "소속 기수 목록", example = "[6, 7]") + val cardinals: List, + @field:Schema(description = "권한", example = "USER", nullable = true) + val role: Role?, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserSummaryResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserSummaryResponse.kt new file mode 100644 index 00000000..4bd1e31e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserSummaryResponse.kt @@ -0,0 +1,15 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.user.domain.entity.enums.Role +import io.swagger.v3.oas.annotations.media.Schema + +data class UserSummaryResponse( + @field:Schema(description = "사용자 ID", example = "1") + val id: Long, + @field:Schema(description = "이름", example = "홍길동") + val name: String, + @field:Schema(description = "소속 기수 목록", example = "[6, 7]") + val cardinals: List, + @field:Schema(description = "권한", example = "USER", nullable = true) + val role: Role?, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/CardinalNotFoundException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/CardinalNotFoundException.kt new file mode 100644 index 00000000..94ea712f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/CardinalNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class CardinalNotFoundException : BaseException(UserErrorCode.CARDINAL_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/DuplicateCardinalException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/DuplicateCardinalException.kt new file mode 100644 index 00000000..8cacbd14 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/DuplicateCardinalException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class DuplicateCardinalException : BaseException(UserErrorCode.DUPLICATE_CARDINAL) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/EmailNotFoundException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/EmailNotFoundException.kt new file mode 100644 index 00000000..0c94746c --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/EmailNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class EmailNotFoundException : BaseException(UserErrorCode.EMAIL_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/InvalidUserOrderException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/InvalidUserOrderException.kt new file mode 100644 index 00000000..635e0293 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/InvalidUserOrderException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class InvalidUserOrderException : BaseException(UserErrorCode.INVALID_USER_ORDER) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/PasswordMismatchException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/PasswordMismatchException.kt new file mode 100644 index 00000000..3e46d078 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/PasswordMismatchException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class PasswordMismatchException : BaseException(UserErrorCode.PASSWORD_MISMATCH) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/RoleNotFoundException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/RoleNotFoundException.kt new file mode 100644 index 00000000..c2608604 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/RoleNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class RoleNotFoundException : BaseException(UserErrorCode.ROLE_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/StatusNotFoundException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/StatusNotFoundException.kt new file mode 100644 index 00000000..d1f7c1c3 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/StatusNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class StatusNotFoundException : BaseException(UserErrorCode.STATUS_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/StudentIdExistsException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/StudentIdExistsException.kt new file mode 100644 index 00000000..f5a74190 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/StudentIdExistsException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class StudentIdExistsException : BaseException(UserErrorCode.STUDENT_ID_EXISTS) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/TelExistsException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/TelExistsException.kt new file mode 100644 index 00000000..aaf8d445 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/TelExistsException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class TelExistsException : BaseException(UserErrorCode.TEL_EXISTS) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/UserCardinalNotFoundException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/UserCardinalNotFoundException.kt new file mode 100644 index 00000000..da49afec --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserCardinalNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class UserCardinalNotFoundException : BaseException(UserErrorCode.USER_CARDINAL_NOT_FOUND) diff --git a/src/main/java/com/weeth/domain/user/application/exception/UserErrorCode.java b/src/main/kotlin/com/weeth/domain/user/application/exception/UserErrorCode.kt similarity index 83% rename from src/main/java/com/weeth/domain/user/application/exception/UserErrorCode.java rename to src/main/kotlin/com/weeth/domain/user/application/exception/UserErrorCode.kt index 9fd45632..12f4af47 100644 --- a/src/main/java/com/weeth/domain/user/application/exception/UserErrorCode.java +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserErrorCode.kt @@ -1,15 +1,14 @@ -package com.weeth.domain.user.application.exception; - -import com.weeth.global.common.exception.ErrorCodeInterface; -import com.weeth.global.common.exception.ExplainError; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum UserErrorCode implements ErrorCodeInterface { - // User 관련 에러 +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.ErrorCodeInterface +import com.weeth.global.common.exception.ExplainError +import org.springframework.http.HttpStatus + +enum class UserErrorCode( + private val code: Int, + private val status: HttpStatus, + private val message: String, +) : ErrorCodeInterface { @ExplainError("사용자 ID로 조회했으나 해당 사용자가 존재하지 않을 때 발생합니다.") USER_NOT_FOUND(2800, HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."), @@ -25,21 +24,18 @@ public enum UserErrorCode implements ErrorCodeInterface { @ExplainError("다른 사용자의 리소스에 접근하려고 할 때 발생합니다.") USER_NOT_MATCH(2804, HttpStatus.FORBIDDEN, "해당 사용자가 아닙니다."), - // 인증 관련 에러 @ExplainError("로그인 시 비밀번호가 일치하지 않을 때 발생합니다.") PASSWORD_MISMATCH(2805, HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), @ExplainError("입력한 이메일로 등록된 사용자가 없을 때 발생합니다.") EMAIL_NOT_FOUND(2806, HttpStatus.NOT_FOUND, "이메일을 찾을 수 없습니다."), - // 검증 에러 @ExplainError("이미 등록된 학번으로 회원가입을 시도할 때 발생합니다.") STUDENT_ID_EXISTS(2807, HttpStatus.BAD_REQUEST, "이미 존재하는 학번입니다."), @ExplainError("이미 등록된 전화번호로 회원가입을 시도할 때 발생합니다.") TEL_EXISTS(2808, HttpStatus.BAD_REQUEST, "이미 존재하는 전화번호입니다."), - // Cardinal 관련 에러 @ExplainError("존재하지 않는 기수 정보로 조회할 때 발생합니다.") CARDINAL_NOT_FOUND(2809, HttpStatus.NOT_FOUND, "기수를 찾을 수 없습니다."), @@ -49,7 +45,6 @@ public enum UserErrorCode implements ErrorCodeInterface { @ExplainError("사용자와 기수 간의 연결 정보를 찾을 수 없을 때 발생합니다.") USER_CARDINAL_NOT_FOUND(2811, HttpStatus.NOT_FOUND, "사용자 기수 정보를 찾을 수 없습니다."), - // Enum 관련 에러 @ExplainError("잘못된 학과 값이 입력되었을 때 발생합니다.") DEPARTMENT_NOT_FOUND(2812, HttpStatus.BAD_REQUEST, "학과를 찾을 수 없습니다."), @@ -60,9 +55,12 @@ public enum UserErrorCode implements ErrorCodeInterface { STATUS_NOT_FOUND(2814, HttpStatus.BAD_REQUEST, "상태를 찾을 수 없습니다."), @ExplainError("사용자 순서 지정 시 잘못된 값이 입력되었을 때 발생합니다.") - INVALID_USER_ORDER(2815, HttpStatus.BAD_REQUEST, "잘못된 사용자 순서입니다."); + INVALID_USER_ORDER(2815, HttpStatus.BAD_REQUEST, "잘못된 사용자 순서입니다."), + ; + + override fun getCode(): Int = code + + override fun getStatus(): HttpStatus = status - private final int code; - private final HttpStatus status; - private final String message; + override fun getMessage(): String = message } diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/UserExistsException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/UserExistsException.kt new file mode 100644 index 00000000..ece5509d --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserExistsException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class UserExistsException : BaseException(UserErrorCode.USER_EXISTS) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/UserInActiveException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/UserInActiveException.kt new file mode 100644 index 00000000..5f639651 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserInActiveException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class UserInActiveException : BaseException(UserErrorCode.USER_INACTIVE) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/UserMismatchException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/UserMismatchException.kt new file mode 100644 index 00000000..20db334e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserMismatchException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class UserMismatchException : BaseException(UserErrorCode.USER_MISMATCH) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/UserNotFoundException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/UserNotFoundException.kt new file mode 100644 index 00000000..9ff4caf9 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class UserNotFoundException : BaseException(UserErrorCode.USER_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/user/application/exception/UserNotMatchException.kt b/src/main/kotlin/com/weeth/domain/user/application/exception/UserNotMatchException.kt new file mode 100644 index 00000000..d058919e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/exception/UserNotMatchException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.user.application.exception + +import com.weeth.global.common.exception.BaseException + +class UserNotMatchException : BaseException(UserErrorCode.USER_NOT_MATCH) diff --git a/src/main/kotlin/com/weeth/domain/user/application/mapper/CardinalMapper.kt b/src/main/kotlin/com/weeth/domain/user/application/mapper/CardinalMapper.kt new file mode 100644 index 00000000..a0fb3b74 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/mapper/CardinalMapper.kt @@ -0,0 +1,27 @@ +package com.weeth.domain.user.application.mapper + +import com.weeth.domain.user.application.dto.request.CardinalSaveRequest +import com.weeth.domain.user.application.dto.response.CardinalResponse +import com.weeth.domain.user.domain.entity.Cardinal +import org.springframework.stereotype.Component + +@Component +class CardinalMapper { + fun toEntity(request: CardinalSaveRequest): Cardinal = + Cardinal( + cardinalNumber = request.cardinalNumber, + year = request.year, + semester = request.semester, + ) + + fun toResponse(cardinal: Cardinal): CardinalResponse = + CardinalResponse( + cardinal.id, + cardinal.cardinalNumber, + cardinal.year, + cardinal.semester, + cardinal.status, + cardinal.createdAt, + cardinal.modifiedAt, + ) +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMapper.kt b/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMapper.kt new file mode 100644 index 00000000..63c25ff2 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMapper.kt @@ -0,0 +1,104 @@ +package com.weeth.domain.user.application.mapper + +import com.weeth.domain.user.application.dto.request.SignUpRequest +import com.weeth.domain.user.application.dto.response.AdminUserResponse +import com.weeth.domain.user.application.dto.response.UserDetailsResponse +import com.weeth.domain.user.application.dto.response.UserInfoResponse +import com.weeth.domain.user.application.dto.response.UserProfileResponse +import com.weeth.domain.user.application.dto.response.UserSummaryResponse +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal +import org.springframework.stereotype.Component + +@Component +class UserMapper { + fun toEntity(request: SignUpRequest): User = + User.create( + name = request.name, + email = request.email, + studentId = request.studentId, + tel = request.tel, + department = request.department, + ) + + fun toUserProfileResponse( + user: User, + userCardinals: List, + ): UserProfileResponse = + UserProfileResponse( + user.id, + user.name, + user.emailValue, + user.studentId, + user.telValue, + user.department, + toCardinalNumbers(userCardinals), + user.role, + ) + + fun toAdminUserResponse( + user: User, + userCardinals: List, + ): AdminUserResponse = + AdminUserResponse( + user.id, + user.name, + user.emailValue, + user.studentId, + user.telValue, + user.department, + toCardinalNumbers(userCardinals), + user.status, + user.role, + user.attendanceCount, + user.absenceCount, + user.attendanceRate, + user.penaltyCount, + user.warningCount, + user.createdAt, + user.modifiedAt, + ) + + fun toUserSummaryResponse( + user: User, + userCardinals: List, + ): UserSummaryResponse = + UserSummaryResponse( + user.id, + user.name, + toCardinalNumbers(userCardinals), + user.role, + ) + + fun toUserDetailsResponse( + user: User, + userCardinals: List, + ): UserDetailsResponse = + UserDetailsResponse( + user.id, + user.name, + user.emailValue, + user.studentId, + user.department, + toCardinalNumbers(userCardinals), + user.role, + ) + + fun toUserInfoResponse( + user: User, + userCardinals: List, + ): UserInfoResponse = + UserInfoResponse( + user.id, + user.name, + toCardinalNumbers(userCardinals), + user.role, + ) + + private fun toCardinalNumbers(userCardinals: List): List { + if (userCardinals.isEmpty()) { + return emptyList() + } + return userCardinals.map { it.cardinal.cardinalNumber } + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt new file mode 100644 index 00000000..df19f100 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt @@ -0,0 +1,107 @@ +package com.weeth.domain.user.application.usecase.command + +import com.weeth.domain.attendance.domain.service.AttendanceSaveService +import com.weeth.domain.schedule.domain.entity.Meeting +import com.weeth.domain.schedule.domain.service.MeetingGetService +import com.weeth.domain.user.application.dto.request.UserApplyObRequest +import com.weeth.domain.user.application.dto.request.UserIdsRequest +import com.weeth.domain.user.application.dto.request.UserRoleUpdateRequest +import com.weeth.domain.user.application.exception.CardinalNotFoundException +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal +import com.weeth.domain.user.domain.repository.CardinalRepository +import com.weeth.domain.user.domain.repository.UserCardinalRepository +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.service.UserCardinalPolicy +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AdminUserUseCase( + private val userReader: UserReader, + private val attendanceSaveService: AttendanceSaveService, + private val meetingGetService: MeetingGetService, + private val cardinalRepository: CardinalRepository, + private val userCardinalRepository: UserCardinalRepository, + private val userCardinalPolicy: UserCardinalPolicy, +) { + @Transactional + fun accept(request: UserIdsRequest) { + val users = userReader.findAllByIds(request.userId) + users.forEach { user -> + val cardinal = userCardinalPolicy.getCurrentCardinal(user).cardinalNumber + if (user.isInactive()) { + user.accept() + val meetings: List = meetingGetService.find(cardinal) + attendanceSaveService.init(user, meetings) + } + } + } + + @Transactional + fun updateRole(request: List) { + request.forEach { req -> + val user = userReader.getById(req.userId) + user.updateRole(req.role) + } + } + + @Transactional + fun ban(request: UserIdsRequest) { + val users = userReader.findAllByIds(request.userId) + users.forEach { user -> + user.ban() + } + } + + @Transactional + fun applyOb(requests: List) { // todo: 리팩토링 + if (requests.isEmpty()) return + + val distinctUserIds = requests.map { it.userId }.distinct() + val users = userReader.findAllByIds(distinctUserIds) + val userMap = users.associateBy { it.id } + distinctUserIds.firstOrNull { it !in userMap }?.let { userReader.getById(it) } + + val existingCardinalsByUser = userCardinalRepository.findAllByUsers(users).groupBy { it.user.id } + val cardinalMap = getOrCreateCardinals(requests.map { it.cardinal }.distinct()) + + val newLinks = mutableListOf>() + val initNeededByCardinal = mutableMapOf>() + + requests.forEach { req -> + val user = userMap.getValue(req.userId) + val nextCardinal = cardinalMap.getValue(req.cardinal) + val existing = existingCardinalsByUser[user.id] ?: emptyList() + + if (existing.any { it.cardinal.id == nextCardinal.id }) return@forEach + + val maxCardinalNumber = + existing.maxOfOrNull { it.cardinal.cardinalNumber } ?: throw CardinalNotFoundException() + + if (maxCardinalNumber < nextCardinal.cardinalNumber) { + user.resetAttendanceStats() + initNeededByCardinal.getOrPut(req.cardinal) { mutableListOf() }.add(user) + } + newLinks.add(user to nextCardinal) + } + + if (initNeededByCardinal.isNotEmpty()) { + val meetingsMap = meetingGetService.findByCardinals(initNeededByCardinal.keys.toList()) + initNeededByCardinal.forEach { (cardinalNumber, usersToInit) -> + val meetings = meetingsMap[cardinalNumber] ?: emptyList() + usersToInit.forEach { attendanceSaveService.init(it, meetings) } + } + } + + newLinks.forEach { (user, cardinal) -> userCardinalRepository.save(UserCardinal(user, cardinal)) } + } + + private fun getOrCreateCardinals(cardinalNumbers: List): Map { + val existing = cardinalRepository.findAllByCardinalNumberIn(cardinalNumbers).associateBy { it.cardinalNumber } + return cardinalNumbers.associateWith { num -> + existing[num] ?: cardinalRepository.save(Cardinal.create(cardinalNumber = num)) + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AuthUserUseCase.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AuthUserUseCase.kt new file mode 100644 index 00000000..75a139a1 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AuthUserUseCase.kt @@ -0,0 +1,231 @@ +package com.weeth.domain.user.application.usecase.command + +import com.weeth.domain.user.application.dto.request.SignUpRequest +import com.weeth.domain.user.application.dto.request.SocialLoginRequest +import com.weeth.domain.user.application.dto.request.UpdateUserProfileRequest +import com.weeth.domain.user.application.dto.response.SocialLoginResponse +import com.weeth.domain.user.application.exception.EmailNotFoundException +import com.weeth.domain.user.application.exception.StudentIdExistsException +import com.weeth.domain.user.application.exception.TelExistsException +import com.weeth.domain.user.application.exception.UserInActiveException +import com.weeth.domain.user.application.mapper.UserMapper +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal +import com.weeth.domain.user.domain.entity.UserSocialAccount +import com.weeth.domain.user.domain.entity.enums.SocialProvider +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalRepository +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.repository.UserRepository +import com.weeth.domain.user.domain.repository.UserSocialAccountRepository +import com.weeth.global.auth.apple.AppleAuthService +import com.weeth.global.auth.jwt.application.dto.JwtDto +import com.weeth.global.auth.jwt.application.service.JwtTokenExtractor +import com.weeth.global.auth.jwt.application.usecase.JwtManageUseCase +import com.weeth.global.auth.kakao.KakaoAuthService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AuthUserUseCase( + private val userRepository: UserRepository, + private val userReader: UserReader, + private val cardinalReader: CardinalReader, + private val userCardinalRepository: UserCardinalRepository, + private val mapper: UserMapper, + private val userSocialAccountRepository: UserSocialAccountRepository, + private val kakaoAuthService: KakaoAuthService, + private val appleAuthService: AppleAuthService, + private val jwtManageUseCase: JwtManageUseCase, + private val jwtTokenExtractor: JwtTokenExtractor, +) { + @Transactional + fun updateProfile( + request: UpdateUserProfileRequest, + userId: Long, + ) { + validate(request, userId) + val user = userReader.getById(userId) + user.update(request.name, request.email, request.studentId, request.tel, request.department) + } + + @Transactional + fun apply(request: SignUpRequest) { // todo: 리팩토링 + validate(request) + val cardinal = cardinalReader.getByCardinalNumber(request.cardinal) + val user = mapper.toEntity(request) + val userCardinal = UserCardinal(user, cardinal) + + userRepository.save(user) + userCardinalRepository.save(userCardinal) + } + + @Transactional + fun leave(userId: Long) { + val user = userReader.getById(userId) + user.leave() + } + + @Transactional + fun socialLoginByKakao(request: SocialLoginRequest): SocialLoginResponse { // todo: 리팩토링 + val kakaoToken = kakaoAuthService.getKakaoToken(request.authCode) + val userInfo = kakaoAuthService.getUserInfo(kakaoToken.accessToken) + val account = userInfo.kakaoAccount + val email = account.email?.trim()?.lowercase() + val providerName = + account.profile + ?.nickname + ?.trim() + ?.takeIf { it.isNotBlank() } + if (!account.isEmailValid || !account.isEmailVerified || email.isNullOrBlank()) { + throw EmailNotFoundException() + } + return loginOrCreate( + provider = SocialProvider.KAKAO, + providerUserId = userInfo.id.toString(), + providerEmail = email, + providerName = providerName, + request = request, + ) + } + + @Transactional + fun socialLoginByApple(request: SocialLoginRequest): SocialLoginResponse { // todo: 리팩토링 + val appleToken = appleAuthService.getAppleToken(request.authCode) + val userInfo = appleAuthService.verifyAndDecodeIdToken(appleToken.idToken) + val email = userInfo.email?.trim()?.lowercase() + val providerName = userInfo.name?.trim()?.takeIf { it.isNotBlank() } + if (!userInfo.emailVerified || email.isNullOrBlank()) { + throw EmailNotFoundException() + } + return loginOrCreate( + provider = SocialProvider.APPLE, + providerUserId = userInfo.appleId, + providerEmail = email, + providerName = providerName, + request = request, + ) + } + + fun refreshToken(httpServletRequest: HttpServletRequest): JwtDto { + val refreshToken = jwtTokenExtractor.extractRefreshToken(httpServletRequest) + return jwtManageUseCase.reIssueToken(refreshToken) + } + + private fun validate( + request: UpdateUserProfileRequest, + userId: Long, + ) { + if (userRepository.existsByStudentIdAndIdIsNot(request.studentId, userId)) { + throw StudentIdExistsException() + } + if (userRepository.existsByTelAndIdIsNotValue(request.tel, userId)) { + throw TelExistsException() + } + } + + private fun validate(request: SignUpRequest) { + if (userRepository.existsByStudentId(request.studentId)) { + throw StudentIdExistsException() + } + if (userRepository.existsByTelValue(request.tel)) { + throw TelExistsException() + } + } + + private fun loginOrCreate( + provider: SocialProvider, + providerUserId: String, + providerEmail: String, + providerName: String?, + request: SocialLoginRequest, + ): SocialLoginResponse { + val socialAccount = userSocialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId).orElse(null) + val (user, isNewUser) = + if (socialAccount != null) { + socialAccount.user to false + } else { + createAndPersistSocialAccount(provider, providerUserId, providerEmail, providerName) + } + + if (user.status == Status.BANNED || user.status == Status.LEFT) { + throw UserInActiveException() + } + + val hasExplicitPayload = + request.name != null || + request.studentId != null || + request.tel != null || + request.department != null + + if (isNewUser || hasExplicitPayload) { + applyOptionalProfile(user, request, providerName) + } + + val token = jwtManageUseCase.create(user.id, user.emailValue, user.role) + return SocialLoginResponse( + email = user.emailValue, + accessToken = token.accessToken, + refreshToken = token.refreshToken, + isNewUser = isNewUser, + profileCompleted = user.isProfileCompleted(), + ) + } + + private fun createAndPersistSocialAccount( + provider: SocialProvider, + providerUserId: String, + providerEmail: String, + providerName: String?, + ): Pair { + val existingUser = userRepository.findByEmailValue(providerEmail).orElse(null) + val user = + existingUser ?: userRepository.save( + User.create( + name = providerName ?: "", + email = providerEmail, + studentId = "", + tel = "", + department = "", + ), + ) + userSocialAccountRepository.save(UserSocialAccount(provider = provider, providerUserId = providerUserId, user = user)) + return user to (existingUser == null) + } + + private fun applyOptionalProfile( + user: User, + request: SocialLoginRequest, + providerName: String?, + ) { + val hasProfilePayload = + providerName != null || + request.name != null || + request.studentId != null || + request.tel != null || + request.department != null + if (!hasProfilePayload) { + return + } + + val nextName = request.name?.trim()?.takeIf { it.isNotBlank() } ?: providerName ?: user.name + val nextStudentId = request.studentId ?: user.studentId + val nextTel = request.tel ?: user.telValue + val nextDepartment = request.department ?: user.department + + if ( + nextStudentId != user.studentId && + nextStudentId.isNotBlank() && + userRepository.existsByStudentIdAndIdIsNot(nextStudentId, user.id) + ) { + throw StudentIdExistsException() + } + if (nextTel != user.telValue && nextTel.isNotBlank() && userRepository.existsByTelAndIdIsNotValue(nextTel, user.id)) { + throw TelExistsException() + } + + user.update(nextName, user.emailValue, nextStudentId, nextTel, nextDepartment) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/command/ManageCardinalUseCase.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/ManageCardinalUseCase.kt new file mode 100644 index 00000000..055b9656 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/ManageCardinalUseCase.kt @@ -0,0 +1,46 @@ +package com.weeth.domain.user.application.usecase.command + +import com.weeth.domain.user.application.dto.request.CardinalSaveRequest +import com.weeth.domain.user.application.dto.request.CardinalUpdateRequest +import com.weeth.domain.user.application.exception.CardinalNotFoundException +import com.weeth.domain.user.application.exception.DuplicateCardinalException +import com.weeth.domain.user.application.mapper.CardinalMapper +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.entity.enums.CardinalStatus +import com.weeth.domain.user.domain.repository.CardinalRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageCardinalUseCase( + private val cardinalRepository: CardinalRepository, + private val cardinalMapper: CardinalMapper, +) { + @Transactional + fun save(request: CardinalSaveRequest) { + if (cardinalRepository.findByCardinalNumber(request.cardinalNumber).isPresent) { + throw DuplicateCardinalException() + } + + val cardinal = cardinalRepository.save(cardinalMapper.toEntity(request)) + if (request.inProgress) { + updateCardinalStatus(cardinal) + } + } + + @Transactional + fun update(request: CardinalUpdateRequest) { + val cardinal = cardinalRepository.findById(request.id).orElseThrow { CardinalNotFoundException() } + cardinal.update(request.year, request.semester) + + if (request.inProgress) { + updateCardinalStatus(cardinal) + } + } + + private fun updateCardinalStatus(cardinal: Cardinal) { + val inProgressCardinals = cardinalRepository.findAllByStatus(CardinalStatus.IN_PROGRESS) + inProgressCardinals.forEach(Cardinal::done) + cardinal.inProgress() + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetCardinalQueryService.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetCardinalQueryService.kt new file mode 100644 index 00000000..91d02166 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetCardinalQueryService.kt @@ -0,0 +1,16 @@ +package com.weeth.domain.user.application.usecase.query + +import com.weeth.domain.user.application.dto.response.CardinalResponse +import com.weeth.domain.user.application.mapper.CardinalMapper +import com.weeth.domain.user.domain.repository.CardinalRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class GetCardinalQueryService( + private val cardinalRepository: CardinalRepository, + private val cardinalMapper: CardinalMapper, +) { + fun findAll(): List = cardinalRepository.findAllByOrderByCardinalNumberAsc().map(cardinalMapper::toResponse) +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserQueryService.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserQueryService.kt new file mode 100644 index 00000000..f3ab026f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserQueryService.kt @@ -0,0 +1,115 @@ +package com.weeth.domain.user.application.usecase.query + +import com.weeth.domain.user.application.dto.response.AdminUserResponse +import com.weeth.domain.user.application.dto.response.UserDetailsResponse +import com.weeth.domain.user.application.dto.response.UserInfoResponse +import com.weeth.domain.user.application.dto.response.UserProfileResponse +import com.weeth.domain.user.application.dto.response.UserSummaryResponse +import com.weeth.domain.user.application.mapper.UserMapper +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.entity.enums.StatusPriority +import com.weeth.domain.user.domain.entity.enums.UsersOrderBy +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalRepository +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.repository.UserRepository +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Slice +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.LinkedHashMap + +@Service +@Transactional(readOnly = true) +class GetUserQueryService( + private val userRepository: UserRepository, + private val userReader: UserReader, + private val cardinalReader: CardinalReader, + private val userCardinalRepository: UserCardinalRepository, + private val userCardinalReader: UserCardinalReader, + private val mapper: UserMapper, +) { + fun existsByEmail(email: String): Boolean = userRepository.existsByEmailValue(email) + + fun findAllUser( + pageNumber: Int, + pageSize: Int, + cardinal: Int?, + ): Slice { + val pageable = PageRequest.of(pageNumber, pageSize) + val users: Slice = + if (cardinal == null) { + userRepository.findAllByStatusOrderedByCardinalAndName(Status.ACTIVE, pageable) + } else { + val inputCardinal = cardinalReader.getByCardinalNumber(cardinal) + userRepository.findAllByCardinalOrderByNameAsc(Status.ACTIVE, inputCardinal, pageable) + } + + val allUserCardinals = userCardinalReader.findAllByUsersOrderByCardinalDesc(users.content) + val userCardinalMap = allUserCardinals.groupBy { it.user.id } + return users.map { user -> + val userCardinals = userCardinalMap[user.id] ?: emptyList() + mapper.toUserSummaryResponse(user, userCardinals) + } + } + + fun searchUser(keyword: String): List { + val users = userRepository.findAllByNameContainingAndStatus(keyword, Status.ACTIVE) + val allUserCardinals = userCardinalReader.findAllByUsersOrderByCardinalDesc(users) + val userCardinalMap = allUserCardinals.groupBy { it.user.id } + return users.map { user -> + val userCardinals = userCardinalMap[user.id] ?: emptyList() + mapper.toUserSummaryResponse(user, userCardinals) + } + } + + fun findUserDetails(userId: Long): UserDetailsResponse { + val user = userReader.getById(userId) + val userCardinals = userCardinalReader.findAllByUser(user) + return mapper.toUserDetailsResponse(user, userCardinals) + } + + fun findMyProfile(userId: Long): UserProfileResponse { + val user = userReader.getById(userId) + val userCardinals = userCardinalReader.findAllByUser(user) + return mapper.toUserProfileResponse(user, userCardinals) + } + + fun findMyInfo(userId: Long): UserInfoResponse { + val user = userReader.getById(userId) + val userCardinals = userCardinalReader.findAllByUser(user) + return mapper.toUserInfoResponse(user, userCardinals) + } + + fun findAllByAdmin(orderBy: UsersOrderBy): List { + val userCardinalMap: LinkedHashMap> = + LinkedHashMap( + userCardinalRepository.findAllByOrderByUserNameAsc().groupBy { it.user }, + ) + + return when (orderBy) { + UsersOrderBy.NAME_ASCENDING -> { + userCardinalMap.entries + .sortedBy { StatusPriority.fromStatus(it.key.status).priority } + .map { entry -> + mapper.toAdminUserResponse(entry.key, entry.value) + } + } + + UsersOrderBy.CARDINAL_DESCENDING -> { + userCardinalMap.entries + .sortedWith( + compareBy>> { StatusPriority.fromStatus(it.key.status).priority } + .thenByDescending { entry -> + entry.value.maxOfOrNull { it.cardinal.cardinalNumber } ?: -1 + }, + ).map { entry -> + mapper.toAdminUserResponse(entry.key, entry.value) + } + } + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/converter/EmailConverter.kt b/src/main/kotlin/com/weeth/domain/user/domain/converter/EmailConverter.kt new file mode 100644 index 00000000..836d8745 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/converter/EmailConverter.kt @@ -0,0 +1,12 @@ +package com.weeth.domain.user.domain.converter + +import com.weeth.domain.user.domain.vo.Email +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter + +@Converter(autoApply = false) +class EmailConverter : AttributeConverter { + override fun convertToDatabaseColumn(attribute: Email?): String = attribute?.value ?: "" + + override fun convertToEntityAttribute(dbData: String?): Email = Email.from(dbData ?: "") +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/converter/PhoneNumberConverter.kt b/src/main/kotlin/com/weeth/domain/user/domain/converter/PhoneNumberConverter.kt new file mode 100644 index 00000000..c90c8129 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/converter/PhoneNumberConverter.kt @@ -0,0 +1,12 @@ +package com.weeth.domain.user.domain.converter + +import com.weeth.domain.user.domain.vo.PhoneNumber +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter + +@Converter(autoApply = false) +class PhoneNumberConverter : AttributeConverter { + override fun convertToDatabaseColumn(attribute: PhoneNumber?): String = attribute?.value ?: "" + + override fun convertToEntityAttribute(dbData: String?): PhoneNumber = PhoneNumber.from(dbData ?: "") +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/Cardinal.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/Cardinal.kt new file mode 100644 index 00000000..d5c3cf42 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/Cardinal.kt @@ -0,0 +1,56 @@ +package com.weeth.domain.user.domain.entity + +import com.weeth.domain.user.domain.entity.enums.CardinalStatus +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity +class Cardinal( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "cardinal_id") + val id: Long = 0L, + @Column(unique = true, nullable = false) + val cardinalNumber: Int, + var year: Int? = null, + var semester: Int? = null, + @Enumerated(EnumType.STRING) + var status: CardinalStatus = CardinalStatus.DONE, +) : BaseEntity() { + fun update( + year: Int, + semester: Int, + ) { + this.year = year + this.semester = semester + } + + fun inProgress() { + status = CardinalStatus.IN_PROGRESS + } + + fun done() { + status = CardinalStatus.DONE + } + + companion object { + fun create( + cardinalNumber: Int, + year: Int? = null, + semester: Int? = null, + status: CardinalStatus = CardinalStatus.DONE, + ): Cardinal = + Cardinal( + cardinalNumber = cardinalNumber, + year = year, + semester = semester, + status = status, + ) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/User.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/User.kt new file mode 100644 index 00000000..b2b27bab --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/User.kt @@ -0,0 +1,196 @@ +package com.weeth.domain.user.domain.entity + +import com.weeth.domain.user.domain.converter.EmailConverter +import com.weeth.domain.user.domain.converter.PhoneNumberConverter +import com.weeth.domain.user.domain.entity.enums.Role +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.vo.AttendanceStats +import com.weeth.domain.user.domain.vo.Email +import com.weeth.domain.user.domain.vo.PhoneNumber +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.PrePersist +import jakarta.persistence.Table + +@Entity +@Table(name = "users") +class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + var id: Long = 0L, + var name: String = "", + @Convert(converter = EmailConverter::class) + @Column(name = "email") + var email: Email = Email.from(""), + var studentId: String = "", + @Convert(converter = PhoneNumberConverter::class) + @Column(name = "tel") + var tel: PhoneNumber = PhoneNumber.from(""), + var department: String = "", + @Enumerated(EnumType.STRING) + var status: Status = Status.WAITING, + @Enumerated(EnumType.STRING) + var role: Role = Role.USER, + @Embedded + var attendanceStats: AttendanceStats = AttendanceStats(), + var penaltyCount: Int = 0, + var warningCount: Int = 0, // todo: 경고시 자동 페널티 기능도 제거 +) : BaseEntity() { + constructor( + id: Long = 0L, + name: String = "", + email: String = "", + studentId: String = "", + tel: String = "", + department: String = "", + status: Status = Status.WAITING, + role: Role = Role.USER, + attendanceCount: Int = 0, + absenceCount: Int = 0, + attendanceRate: Int = 0, + penaltyCount: Int = 0, + warningCount: Int = 0, + ) : this( + id = id, + name = name, + email = Email.from(email), + studentId = studentId, + tel = PhoneNumber.from(tel), + department = department, + status = status, + role = role, + attendanceStats = AttendanceStats(attendanceCount, absenceCount, attendanceRate), + penaltyCount = penaltyCount, + warningCount = warningCount, + ) + + val emailValue: String + get() = email.value + + val telValue: String + get() = tel.value + + val attendanceCount: Int + get() = attendanceStats.attendanceCount + + val absenceCount: Int + get() = attendanceStats.absenceCount + + val attendanceRate: Int + get() = attendanceStats.attendanceRate + + @PrePersist + fun init() { + status = Status.WAITING + role = Role.USER + attendanceStats.reset() + penaltyCount = 0 + warningCount = 0 + } + + fun leave() { + status = Status.LEFT + } + + fun isInactive(): Boolean = status != Status.ACTIVE + + fun isProfileCompleted(): Boolean = + name.isNotBlank() && + studentId.isNotBlank() && + telValue.isNotBlank() && + department.isNotBlank() + + fun update( + name: String, + email: String, + studentId: String, + tel: String, + department: String, + ) { + this.name = name + this.email = Email.from(email) + this.studentId = studentId + this.tel = PhoneNumber.from(tel) + this.department = department + } + + fun accept() { + status = Status.ACTIVE + } + + fun ban() { + status = Status.BANNED + } + + fun updateRole(role: Role) { + this.role = role + } + + fun resetAttendanceStats() { + attendanceStats.reset() + } + + fun attend() { + attendanceStats.attend() + } + + fun removeAttend() { + attendanceStats.removeAttend() + } + + fun absent() { + attendanceStats.absent() + } + + fun removeAbsent() { + attendanceStats.removeAbsent() + } + + fun incrementPenaltyCount() { + penaltyCount++ + } + + fun decrementPenaltyCount() { + if (penaltyCount > 0) { + penaltyCount-- + } + } + + fun incrementWarningCount() { + warningCount++ + } + + fun decrementWarningCount() { + if (warningCount > 0) { + warningCount-- + } + } + + fun hasRole(role: Role): Boolean = this.role == role + + companion object { + fun create( + name: String, + email: String, + studentId: String, + tel: String, + department: String, + ): User = + User( + name = name, + email = Email.from(email), + studentId = studentId, + tel = PhoneNumber.from(tel), + department = department, + ) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/UserCardinal.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/UserCardinal.kt new file mode 100644 index 00000000..c33c346b --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/UserCardinal.kt @@ -0,0 +1,34 @@ +package com.weeth.domain.user.domain.entity + +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +@Entity +class UserCardinal( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_cardinal_id") + val id: Long = 0L, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + val user: User, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cardinal_id") + val cardinal: Cardinal, +) : BaseEntity() { + constructor( + user: User, + cardinal: Cardinal, + ) : this( + id = 0L, + user = user, + cardinal = cardinal, + ) +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/UserSocialAccount.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/UserSocialAccount.kt new file mode 100644 index 00000000..29a27557 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/UserSocialAccount.kt @@ -0,0 +1,41 @@ +package com.weeth.domain.user.domain.entity + +import com.weeth.domain.user.domain.entity.enums.SocialProvider +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint + +@Entity +@Table( + name = "user_social_account", + uniqueConstraints = [ + UniqueConstraint( + name = "uk_provider_provider_user_id", + columnNames = ["provider", "provider_user_id"], + ), + ], +) +class UserSocialAccount( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_social_account_id") + val id: Long = 0L, + @Enumerated(EnumType.STRING) + @Column(nullable = false) + val provider: SocialProvider, + @Column(name = "provider_user_id", nullable = false) + val providerUserId: String, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + val user: User, +) : BaseEntity() diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/CardinalStatus.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/CardinalStatus.kt new file mode 100644 index 00000000..8f009d0e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/CardinalStatus.kt @@ -0,0 +1,6 @@ +package com.weeth.domain.user.domain.entity.enums + +enum class CardinalStatus { + IN_PROGRESS, + DONE, +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/Role.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/Role.kt new file mode 100644 index 00000000..03fe532a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/Role.kt @@ -0,0 +1,6 @@ +package com.weeth.domain.user.domain.entity.enums + +enum class Role { + USER, + ADMIN, +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/SocialProvider.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/SocialProvider.kt new file mode 100644 index 00000000..3bb93fa2 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/SocialProvider.kt @@ -0,0 +1,6 @@ +package com.weeth.domain.user.domain.entity.enums + +enum class SocialProvider { + KAKAO, + APPLE, +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/Status.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/Status.kt new file mode 100644 index 00000000..655dfea6 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/Status.kt @@ -0,0 +1,8 @@ +package com.weeth.domain.user.domain.entity.enums + +enum class Status { + WAITING, + ACTIVE, + BANNED, + LEFT, +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/StatusPriority.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/StatusPriority.kt new file mode 100644 index 00000000..299cf814 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/StatusPriority.kt @@ -0,0 +1,26 @@ +package com.weeth.domain.user.domain.entity.enums + +import com.weeth.domain.user.application.exception.StatusNotFoundException + +enum class StatusPriority( + val priority: Int, +) { + ACTIVE(1), + WAITING(2), + LEFT(3), + BANNED(4), + ; + + companion object { + @JvmStatic + fun from(status: Status?): StatusPriority { + if (status == null) { + throw StatusNotFoundException() + } + return valueOf(status.name) + } + + @JvmStatic + fun fromStatus(status: Status?): StatusPriority = from(status) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/UsersOrderBy.kt b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/UsersOrderBy.kt new file mode 100644 index 00000000..c8ce2584 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/entity/enums/UsersOrderBy.kt @@ -0,0 +1,6 @@ +package com.weeth.domain.user.domain.entity.enums + +enum class UsersOrderBy { + NAME_ASCENDING, + CARDINAL_DESCENDING, +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt new file mode 100644 index 00000000..ae02e8bf --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt @@ -0,0 +1,11 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.domain.entity.Cardinal + +interface CardinalReader { + fun getByCardinalNumber(cardinalNumber: Int): Cardinal + + fun findByIdOrNull(cardinalId: Long): Cardinal? + + fun findAllByCardinalNumberDesc(): List +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt new file mode 100644 index 00000000..0fd0d865 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt @@ -0,0 +1,33 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.application.exception.CardinalNotFoundException +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.entity.enums.CardinalStatus +import org.springframework.data.jpa.repository.JpaRepository +import java.util.Optional + +interface CardinalRepository : + JpaRepository, + CardinalReader { + fun findByCardinalNumber(cardinal: Int): Optional + + fun findAllByCardinalNumberIn(cardinalNumbers: List): List + + fun findByYearAndSemester( + year: Int, + semester: Int, + ): Optional + + fun findAllByStatus(cardinalStatus: CardinalStatus): List + + fun findAllByOrderByCardinalNumberAsc(): List + + fun findAllByOrderByCardinalNumberDesc(): List + + override fun getByCardinalNumber(cardinalNumber: Int): Cardinal = + findByCardinalNumber(cardinalNumber).orElseThrow { CardinalNotFoundException() } + + override fun findByIdOrNull(cardinalId: Long): Cardinal? = findById(cardinalId).orElse(null) + + override fun findAllByCardinalNumberDesc(): List = findAllByOrderByCardinalNumberDesc() +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserCardinalReader.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserCardinalReader.kt new file mode 100644 index 00000000..0e4c25b4 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserCardinalReader.kt @@ -0,0 +1,12 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal + +interface UserCardinalReader { + fun findAllByUser(user: User): List + + fun findAllByUsersOrderByCardinalDesc(users: List): List + + fun findTopByUserOrderByCardinalNumberDesc(user: User): UserCardinal? +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserCardinalRepository.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserCardinalRepository.kt new file mode 100644 index 00000000..b637eb94 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserCardinalRepository.kt @@ -0,0 +1,40 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param + +interface UserCardinalRepository : + JpaRepository, + UserCardinalReader { + fun findAllByUserOrderByCardinalCardinalNumberDesc(user: User): List + + fun findTopByUserOrderByCardinalCardinalNumberDesc(user: User): UserCardinal? + + @Query("SELECT uc FROM UserCardinal uc WHERE uc.user IN :users ORDER BY uc.user.id, uc.cardinal.cardinalNumber DESC") + fun findAllByUsers( + @Param("users") users: List, + ): List + + fun findAllByOrderByUserNameAsc(): List + + @Query( + """ + select uc.cardinal.cardinalNumber + from UserCardinal uc + where uc.user = :user + order by uc.cardinal.cardinalNumber desc + """, + ) + fun findCardinalNumbersByUser( + @Param("user") user: User, + ): List + + override fun findAllByUser(user: User): List = findAllByUserOrderByCardinalCardinalNumberDesc(user) + + override fun findAllByUsersOrderByCardinalDesc(users: List): List = findAllByUsers(users) + + override fun findTopByUserOrderByCardinalNumberDesc(user: User): UserCardinal? = findTopByUserOrderByCardinalCardinalNumberDesc(user) +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt new file mode 100644 index 00000000..8e7c60fb --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt @@ -0,0 +1,13 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.domain.entity.User + +interface UserReader { + fun getById(userId: Long): User + + fun getByEmail(email: String): User + + fun findByIdOrNull(userId: Long): User? + + fun findAllByIds(userIds: List): List +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt new file mode 100644 index 00000000..47ec7f96 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt @@ -0,0 +1,112 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.application.exception.UserNotFoundException +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.vo.Email +import com.weeth.domain.user.domain.vo.PhoneNumber +import jakarta.persistence.LockModeType +import jakarta.persistence.QueryHint +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.data.jpa.repository.QueryHints +import org.springframework.data.repository.query.Param +import java.util.Optional + +interface UserRepository : + JpaRepository, + UserReader { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")) + @Query("SELECT u FROM User u WHERE u.id = :id") + fun findByIdWithLock( + @Param("id") id: Long, + ): Optional + + fun findByEmail(email: Email): Optional + + fun findAllByNameContainingAndStatus( + name: String, + status: Status, + ): List + + fun existsByEmail(email: Email): Boolean + + fun existsByStudentId(studentId: String): Boolean + + fun existsByTel(tel: PhoneNumber): Boolean + + fun existsByStudentIdAndIdIsNot( + studentId: String, + id: Long, + ): Boolean + + fun existsByTelAndIdIsNot( + tel: PhoneNumber, + id: Long, + ): Boolean + + fun findAllByStatusOrderByName(status: Status): List + + fun findAllByOrderByNameAsc(): List + + @Query("SELECT uc.user FROM UserCardinal uc WHERE uc.cardinal = :cardinal AND uc.user.status = :status") + fun findAllByCardinalAndStatus( + @Param("cardinal") cardinal: Cardinal, + @Param("status") status: Status, + ): List + + @Query( + """ + SELECT u + FROM User u + JOIN UserCardinal uc ON u.id = uc.user.id + JOIN uc.cardinal c + WHERE u.status = :status + GROUP BY u.id + ORDER BY MAX(c.cardinalNumber) DESC, u.name ASC + """, + ) + fun findAllByStatusOrderedByCardinalAndName( + @Param("status") status: Status, + pageable: Pageable, + ): Slice + + @Query( + """ + SELECT u FROM User u + JOIN UserCardinal uc ON uc.user.id = u.id + WHERE u.status = :status + AND uc.cardinal = :cardinal + ORDER BY u.name ASC + """, + ) + fun findAllByCardinalOrderByNameAsc( + @Param("status") status: Status, + @Param("cardinal") cardinal: Cardinal, + pageable: Pageable, + ): Slice + + fun findByEmailValue(email: String): Optional = findByEmail(Email.from(email)) + + fun existsByEmailValue(email: String): Boolean = existsByEmail(Email.from(email)) + + fun existsByTelValue(tel: String): Boolean = existsByTel(PhoneNumber.from(tel)) + + fun existsByTelAndIdIsNotValue( + tel: String, + id: Long, + ): Boolean = existsByTelAndIdIsNot(PhoneNumber.from(tel), id) + + override fun getById(userId: Long): User = findById(userId).orElseThrow { UserNotFoundException() } + + override fun getByEmail(email: String): User = findByEmailValue(email).orElseThrow { UserNotFoundException() } + + override fun findByIdOrNull(userId: Long): User? = findById(userId).orElse(null) + + override fun findAllByIds(userIds: List): List = findAllById(userIds) +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserSocialAccountRepository.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserSocialAccountRepository.kt new file mode 100644 index 00000000..d5a4e4db --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserSocialAccountRepository.kt @@ -0,0 +1,13 @@ +package com.weeth.domain.user.domain.repository + +import com.weeth.domain.user.domain.entity.UserSocialAccount +import com.weeth.domain.user.domain.entity.enums.SocialProvider +import org.springframework.data.jpa.repository.JpaRepository +import java.util.Optional + +interface UserSocialAccountRepository : JpaRepository { + fun findByProviderAndProviderUserId( + provider: SocialProvider, + providerUserId: String, + ): Optional +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/service/UserCardinalPolicy.kt b/src/main/kotlin/com/weeth/domain/user/domain/service/UserCardinalPolicy.kt new file mode 100644 index 00000000..2b0a8e3c --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/service/UserCardinalPolicy.kt @@ -0,0 +1,28 @@ +package com.weeth.domain.user.domain.service + +import com.weeth.domain.user.application.exception.CardinalNotFoundException +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.repository.UserCardinalReader +import org.springframework.stereotype.Service + +@Service +class UserCardinalPolicy( + private val userCardinalReader: UserCardinalReader, +) { + fun getCurrentCardinal(user: User): Cardinal = + userCardinalReader + .findTopByUserOrderByCardinalNumberDesc(user) + ?.cardinal + ?: throw CardinalNotFoundException() + + fun notContains( + user: User, + cardinal: Cardinal, + ): Boolean = userCardinalReader.findAllByUser(user).none { it.cardinal.id == cardinal.id } + + fun isCurrent( + user: User, + cardinal: Cardinal, + ): Boolean = getCurrentCardinal(user).cardinalNumber < cardinal.cardinalNumber +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/vo/AttendanceStats.kt b/src/main/kotlin/com/weeth/domain/user/domain/vo/AttendanceStats.kt new file mode 100644 index 00000000..12c357ee --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/vo/AttendanceStats.kt @@ -0,0 +1,49 @@ +package com.weeth.domain.user.domain.vo + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +class AttendanceStats( + @Column(name = "attendance_count") + var attendanceCount: Int = 0, + @Column(name = "absence_count") + var absenceCount: Int = 0, + @Column(name = "attendance_rate") + var attendanceRate: Int = 0, +) { + fun reset() { + attendanceCount = 0 + absenceCount = 0 + attendanceRate = 0 + } + + fun attend() { + attendanceCount++ + recalculateRate() + } + + fun removeAttend() { + if (attendanceCount > 0) { + attendanceCount-- + recalculateRate() + } + } + + fun absent() { + absenceCount++ + recalculateRate() + } + + fun removeAbsent() { + if (absenceCount > 0) { + absenceCount-- + recalculateRate() + } + } + + private fun recalculateRate() { + val total = attendanceCount + absenceCount + attendanceRate = if (total > 0) (attendanceCount * 100) / total else 0 + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/vo/Email.kt b/src/main/kotlin/com/weeth/domain/user/domain/vo/Email.kt new file mode 100644 index 00000000..2b840356 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/vo/Email.kt @@ -0,0 +1,18 @@ +package com.weeth.domain.user.domain.vo + +data class Email private constructor( + val value: String, +) { + companion object { + fun from(raw: String): Email { + val normalized = raw.trim().lowercase() + if (normalized.isBlank()) { + return Email("") + } + require(EMAIL_REGEX.matches(normalized)) { "Invalid email format." } + return Email(normalized) + } + + private val EMAIL_REGEX = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/domain/vo/PhoneNumber.kt b/src/main/kotlin/com/weeth/domain/user/domain/vo/PhoneNumber.kt new file mode 100644 index 00000000..743dcf7c --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/domain/vo/PhoneNumber.kt @@ -0,0 +1,16 @@ +package com.weeth.domain.user.domain.vo + +data class PhoneNumber private constructor( + val value: String, +) { + companion object { + fun from(raw: String): PhoneNumber { + val normalized = raw.filter { it.isDigit() } + if (normalized.isBlank()) { + return PhoneNumber("") + } + require(normalized.length in 10..11) { "Invalid phone number format." } + return PhoneNumber(normalized) + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/presentation/CardinalController.kt b/src/main/kotlin/com/weeth/domain/user/presentation/CardinalController.kt new file mode 100644 index 00000000..740b06b5 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/presentation/CardinalController.kt @@ -0,0 +1,52 @@ +package com.weeth.domain.user.presentation + +import com.weeth.domain.user.application.dto.request.CardinalSaveRequest +import com.weeth.domain.user.application.dto.request.CardinalUpdateRequest +import com.weeth.domain.user.application.dto.response.CardinalResponse +import com.weeth.domain.user.application.exception.UserErrorCode +import com.weeth.domain.user.application.usecase.command.ManageCardinalUseCase +import com.weeth.domain.user.application.usecase.query.GetCardinalQueryService +import com.weeth.global.auth.jwt.application.exception.JwtErrorCode +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.GetMapping +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 + +@Tag(name = "CARDINAL") +@RestController +@RequestMapping("/api/v4") +@ApiErrorCodeExample(UserErrorCode::class, JwtErrorCode::class) +class CardinalController( + private val manageCardinalUseCase: ManageCardinalUseCase, + private val getCardinalQueryService: GetCardinalQueryService, +) { + @GetMapping("/cardinals") + @Operation(summary = "현재 저장된 기수 목록 조회 API") + fun findAllCardinals(): CommonResponse> = + CommonResponse.success(UserResponseCode.CARDINAL_FIND_ALL_SUCCESS, getCardinalQueryService.findAll()) + + @PatchMapping("/admin/cardinals") // todo: 어드민 컨트롤러 분리 + @Operation(summary = "[admin] 기수 정보 수정 API") + fun updateCardinals( + @RequestBody @Valid request: CardinalUpdateRequest, + ): CommonResponse { + manageCardinalUseCase.update(request) + return CommonResponse.success(UserResponseCode.CARDINAL_UPDATE_SUCCESS) + } + + @PostMapping("/admin/cardinals") + @Operation(summary = "[admin] 새로운 기수 정보 저장 API") + fun save( + @RequestBody @Valid request: CardinalSaveRequest, + ): CommonResponse { + manageCardinalUseCase.save(request) + return CommonResponse.success(UserResponseCode.CARDINAL_SAVE_SUCCESS) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/presentation/UserAdminController.kt b/src/main/kotlin/com/weeth/domain/user/presentation/UserAdminController.kt new file mode 100644 index 00000000..e5883150 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/presentation/UserAdminController.kt @@ -0,0 +1,75 @@ +package com.weeth.domain.user.presentation + +import com.weeth.domain.user.application.dto.request.UserApplyObRequest +import com.weeth.domain.user.application.dto.request.UserIdsRequest +import com.weeth.domain.user.application.dto.request.UserRoleUpdateRequest +import com.weeth.domain.user.application.dto.response.AdminUserResponse +import com.weeth.domain.user.application.exception.UserErrorCode +import com.weeth.domain.user.application.usecase.command.AdminUserUseCase +import com.weeth.domain.user.application.usecase.query.GetUserQueryService +import com.weeth.domain.user.domain.entity.enums.UsersOrderBy +import com.weeth.global.auth.jwt.application.exception.JwtErrorCode +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "USER ADMIN", description = "[ADMIN] 사용자 어드민 API") +@RestController +@RequestMapping("/api/v4/admin/users") +@ApiErrorCodeExample(UserErrorCode::class, JwtErrorCode::class) +class UserAdminController( + private val adminUserUseCase: AdminUserUseCase, + private val getUserQueryService: GetUserQueryService, +) { + @GetMapping("/all") + @Operation(summary = "어드민용 회원 조회") + fun findAll( + @RequestParam orderBy: UsersOrderBy, + ): CommonResponse> = + CommonResponse.success(UserResponseCode.USER_FIND_ALL_SUCCESS, getUserQueryService.findAllByAdmin(orderBy)) + + @PatchMapping + @Operation(summary = "가입 신청 승인") + fun accept( + @RequestBody @Valid request: UserIdsRequest, + ): CommonResponse { + adminUserUseCase.accept(request) + return CommonResponse.success(UserResponseCode.USER_ACCEPT_SUCCESS) + } + + @DeleteMapping + @Operation(summary = "유저 추방") + fun ban( + @RequestBody @Valid request: UserIdsRequest, + ): CommonResponse { + adminUserUseCase.ban(request) + return CommonResponse.success(UserResponseCode.USER_BAN_SUCCESS) + } + + @PatchMapping("/role") + @Operation(summary = "관리자로 승격/강등") + fun update( + @RequestBody request: List, + ): CommonResponse { + adminUserUseCase.updateRole(request) + return CommonResponse.success(UserResponseCode.USER_ROLE_UPDATE_SUCCESS) + } + + @PatchMapping("/apply") + @Operation(summary = "다음 기수도 이어서 진행") + fun applyOb( + @RequestBody request: List, + ): CommonResponse { + adminUserUseCase.applyOb(request) + return CommonResponse.success(UserResponseCode.USER_APPLY_OB_SUCCESS) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/presentation/UserController.kt b/src/main/kotlin/com/weeth/domain/user/presentation/UserController.kt new file mode 100644 index 00000000..98ccb3b8 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/presentation/UserController.kt @@ -0,0 +1,134 @@ +package com.weeth.domain.user.presentation + +import com.weeth.domain.user.application.dto.request.SignUpRequest +import com.weeth.domain.user.application.dto.request.SocialLoginRequest +import com.weeth.domain.user.application.dto.request.UpdateUserProfileRequest +import com.weeth.domain.user.application.dto.response.SocialLoginResponse +import com.weeth.domain.user.application.dto.response.UserDetailsResponse +import com.weeth.domain.user.application.dto.response.UserInfoResponse +import com.weeth.domain.user.application.dto.response.UserProfileResponse +import com.weeth.domain.user.application.dto.response.UserSummaryResponse +import com.weeth.domain.user.application.exception.UserErrorCode +import com.weeth.domain.user.application.usecase.command.AdminUserUseCase +import com.weeth.domain.user.application.usecase.command.AuthUserUseCase +import com.weeth.domain.user.application.usecase.query.GetUserQueryService +import com.weeth.global.auth.annotation.CurrentUser +import com.weeth.global.auth.jwt.application.dto.JwtDto +import com.weeth.global.auth.jwt.application.exception.JwtErrorCode +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletRequest +import jakarta.validation.Valid +import org.springframework.data.domain.Slice +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +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.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "USER", description = "사용자 API") +@RestController +@RequestMapping("/api/v4/users") +@ApiErrorCodeExample(UserErrorCode::class, JwtErrorCode::class) +class UserController( + private val authUserUseCase: AuthUserUseCase, + private val adminUserUseCase: AdminUserUseCase, + private val getUserQueryService: GetUserQueryService, +) { + @PostMapping("/social/kakao") + @Operation(summary = "카카오 소셜 로그인(auth code flow)") + fun socialLoginByKakao( + @RequestBody @Valid request: SocialLoginRequest, + ): CommonResponse = + CommonResponse.success(UserResponseCode.SOCIAL_LOGIN_SUCCESS, authUserUseCase.socialLoginByKakao(request)) + + @PostMapping("/social/apple") + @Operation(summary = "애플 소셜 로그인(auth code flow)") + fun socialLoginByApple( + @RequestBody @Valid request: SocialLoginRequest, + ): CommonResponse = + CommonResponse.success(UserResponseCode.SOCIAL_LOGIN_SUCCESS, authUserUseCase.socialLoginByApple(request)) + + @PostMapping("/social/refresh") + @Operation(summary = "토큰 재발급") + fun refreshToken(request: HttpServletRequest): CommonResponse = + CommonResponse.success(UserResponseCode.JWT_REFRESH_SUCCESS, authUserUseCase.refreshToken(request)) + + @PostMapping("/apply") + @Operation(summary = "동아리 지원 신청") + fun apply( + @RequestBody @Valid request: SignUpRequest, + ): CommonResponse { + authUserUseCase.apply(request) + return CommonResponse.success(UserResponseCode.USER_APPLY_SUCCESS) + } + + @GetMapping("/email") + @Operation(summary = "이메일 중복 확인") + fun checkEmail( + @RequestParam email: String, + ): CommonResponse = + CommonResponse.success(UserResponseCode.USER_EMAIL_CHECK_SUCCESS, !getUserQueryService.existsByEmail(email)) + + @GetMapping("/all") + @Operation(summary = "동아리 멤버 전체 조회(전체/기수별)") + fun findAllUser( + @RequestParam("pageNumber") pageNumber: Int, + @RequestParam("pageSize") pageSize: Int, + @RequestParam(required = false) cardinal: Int?, + ): CommonResponse> = + CommonResponse.success(UserResponseCode.USER_FIND_ALL_SUCCESS, getUserQueryService.findAllUser(pageNumber, pageSize, cardinal)) + + @GetMapping("/search") + @Operation(summary = "동아리 멤버 검색") + fun searchUser( + @RequestParam keyword: String, + ): CommonResponse> = + CommonResponse.success(UserResponseCode.USER_FIND_BY_ID_SUCCESS, getUserQueryService.searchUser(keyword)) + + @GetMapping("/details") + @Operation(summary = "특정 멤버 상세 조회") + fun findUser( + @RequestParam userId: Long, + ): CommonResponse = + CommonResponse.success(UserResponseCode.USER_DETAILS_SUCCESS, getUserQueryService.findUserDetails(userId)) + + @GetMapping + @Operation(summary = "내 정보 조회") + fun find( + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse = + CommonResponse.success(UserResponseCode.USER_FIND_BY_ID_SUCCESS, getUserQueryService.findMyProfile(userId)) + + @GetMapping("/info") + @Operation(summary = "전역 내 정보 조회 API") + fun findMyInfo( + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse = + CommonResponse.success(UserResponseCode.USER_FIND_BY_ID_SUCCESS, getUserQueryService.findMyInfo(userId)) + + @PatchMapping + @Operation(summary = "내 정보 수정") + fun update( + @RequestBody @Valid request: UpdateUserProfileRequest, + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse { + authUserUseCase.updateProfile(request, userId) + return CommonResponse.success(UserResponseCode.USER_UPDATE_SUCCESS) + } + + @DeleteMapping + @Operation(summary = "동아리 탈퇴") + fun leave( + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse { + authUserUseCase.leave(userId) + return CommonResponse.success(UserResponseCode.USER_LEAVE_SUCCESS) + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt b/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt new file mode 100644 index 00000000..b7a71b4b --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt @@ -0,0 +1,27 @@ +package com.weeth.domain.user.presentation + +import com.weeth.global.common.response.ResponseCodeInterface +import org.springframework.http.HttpStatus + +enum class UserResponseCode( + override val code: Int, + override val status: HttpStatus, + override val message: String, +) : ResponseCodeInterface { + SOCIAL_LOGIN_SUCCESS(1815, HttpStatus.OK, "소셜 로그인이 성공적으로 처리되었습니다."), + USER_FIND_ALL_SUCCESS(1800, HttpStatus.OK, "모든 회원 정보를 성공적으로 조회했습니다."), + USER_DETAILS_SUCCESS(1801, HttpStatus.OK, "특정 회원의 상세 정보를 성공적으로 조회했습니다."), + USER_ACCEPT_SUCCESS(1802, HttpStatus.OK, "회원 가입 승인이 성공적으로 처리되었습니다."), + USER_BAN_SUCCESS(1803, HttpStatus.OK, "회원이 성공적으로 차단되었습니다."), + USER_ROLE_UPDATE_SUCCESS(1804, HttpStatus.OK, "회원의 역할이 성공적으로 수정되었습니다."), + USER_APPLY_OB_SUCCESS(1805, HttpStatus.OK, "OB 신청이 성공적으로 처리되었습니다."), + USER_APPLY_SUCCESS(1806, HttpStatus.OK, "회원 가입 신청이 성공적으로 처리되었습니다."), + USER_EMAIL_CHECK_SUCCESS(1807, HttpStatus.OK, "이메일 중복 검사가 성공적으로 처리되었습니다."), + USER_FIND_BY_ID_SUCCESS(1808, HttpStatus.OK, "회원 정보가 성공적으로 조회되었습니다."), + USER_UPDATE_SUCCESS(1809, HttpStatus.OK, "회원 정보가 성공적으로 수정되었습니다."), + USER_LEAVE_SUCCESS(1810, HttpStatus.OK, "회원 탈퇴가 성공적으로 처리되었습니다."), + CARDINAL_FIND_ALL_SUCCESS(1811, HttpStatus.OK, "전체 기수 조회에 성공했습니다."), + CARDINAL_SAVE_SUCCESS(1812, HttpStatus.OK, "기수 저장에 성공했습니다."), + CARDINAL_UPDATE_SUCCESS(1813, HttpStatus.OK, "기수 수정에 성공했습니다."), + JWT_REFRESH_SUCCESS(1814, HttpStatus.OK, "토큰 재발급에 성공했습니다."), +} diff --git a/src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt b/src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt index 43e247d6..9130d90e 100644 --- a/src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt +++ b/src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt @@ -112,11 +112,13 @@ class AppleAuthService( val appleId = claims.subject val email = claims.get("email", String::class.java) val emailVerified = parseEmailVerified(claims["email_verified"]) + val name = claims.get("name", String::class.java) return AppleUserInfo( appleId = appleId, email = email, emailVerified = emailVerified, + name = name, ) } catch (e: AppleAuthenticationException) { throw e diff --git a/src/main/kotlin/com/weeth/global/auth/apple/dto/AppleUserInfo.kt b/src/main/kotlin/com/weeth/global/auth/apple/dto/AppleUserInfo.kt index 6678fb98..444de610 100644 --- a/src/main/kotlin/com/weeth/global/auth/apple/dto/AppleUserInfo.kt +++ b/src/main/kotlin/com/weeth/global/auth/apple/dto/AppleUserInfo.kt @@ -4,4 +4,5 @@ data class AppleUserInfo( val appleId: String, val email: String?, val emailVerified: Boolean, + val name: String? = null, ) diff --git a/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoAccount.kt b/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoAccount.kt index da0ca572..1faf46d2 100644 --- a/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoAccount.kt +++ b/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoAccount.kt @@ -9,4 +9,6 @@ data class KakaoAccount( val isEmailVerified: Boolean, @field:JsonProperty("email") val email: String?, + @field:JsonProperty("profile") + val profile: KakaoProfile? = null, ) diff --git a/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoProfile.kt b/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoProfile.kt new file mode 100644 index 00000000..e7ce2ef3 --- /dev/null +++ b/src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoProfile.kt @@ -0,0 +1,8 @@ +package com.weeth.global.auth.kakao.dto + +import com.fasterxml.jackson.annotation.JsonProperty + +data class KakaoProfile( + @field:JsonProperty("nickname") + val nickname: String?, +) diff --git a/src/main/kotlin/com/weeth/global/config/SecurityConfig.kt b/src/main/kotlin/com/weeth/global/config/SecurityConfig.kt index 10dc755f..3db6a33e 100644 --- a/src/main/kotlin/com/weeth/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/weeth/global/config/SecurityConfig.kt @@ -13,8 +13,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.crypto.factory.PasswordEncoderFactories -import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.web.cors.CorsConfiguration @@ -43,14 +41,13 @@ class SecurityConfig( .authorizeHttpRequests { authorize -> authorize .requestMatchers( - "/api/v1/users/kakao/login", - "/api/v1/users/kakao/register", - "/api/v1/users/kakao/link", - "/api/v1/users/apple/login", - "/api/v1/users/apple/register", + "/api/v4/users/apply", + "/api/v4/users/email", + "/api/v4/users/social/kakao", + "/api/v4/users/social/apple", + "/api/v4/users/social/refresh", "/api/v1/users/apply", "/api/v1/users/email", - "/api/v1/users/refresh", ).permitAll() .requestMatchers("/health-check") .permitAll() @@ -105,9 +102,6 @@ class SecurityConfig( } } - @Bean - fun passwordEncoder(): PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() - @Bean fun jwtAuthenticationProcessingFilter(): JwtAuthenticationProcessingFilter = JwtAuthenticationProcessingFilter(jwtTokenProvider, jwtTokenExtractor) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt index ea07505f..bbffddb8 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt @@ -3,43 +3,47 @@ package com.weeth.domain.account.application.usecase.command import com.weeth.domain.account.application.dto.request.AccountSaveRequest import com.weeth.domain.account.application.exception.AccountExistsException import com.weeth.domain.account.domain.repository.AccountRepository -import com.weeth.domain.user.domain.service.CardinalGetService +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.repository.CardinalRepository import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import io.mockk.verify +import java.util.Optional class ManageAccountUseCaseTest : DescribeSpec({ val accountRepository = mockk(relaxed = true) - val cardinalGetService = mockk(relaxUnitFun = true) - val useCase = ManageAccountUseCase(accountRepository, cardinalGetService) + val cardinalRepository = mockk(relaxed = true) + val useCase = ManageAccountUseCase(accountRepository, cardinalRepository) beforeTest { - clearMocks(accountRepository, cardinalGetService) + clearMocks(accountRepository, cardinalRepository) } describe("save") { context("이미 존재하는 기수로 저장 시") { it("AccountExistsException을 던진다") { - val dto = AccountSaveRequest("설명", 100_000, 40) + val request = AccountSaveRequest("설명", 100_000, 40) every { accountRepository.existsByCardinal(40) } returns true - shouldThrow { useCase.save(dto) } + shouldThrow { useCase.save(request) } } } context("정상 저장 시") { - it("account가 저장된다") { - val dto = AccountSaveRequest("설명", 100_000, 40) + it("기수 존재를 보장하고 account를 저장한다") { + val request = AccountSaveRequest("설명", 100_000, 40) every { accountRepository.existsByCardinal(40) } returns false - every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { cardinalRepository.findByCardinalNumber(40) } returns Optional.of(mockk()) every { accountRepository.save(any()) } answers { firstArg() } - useCase.save(dto) + useCase.save(request) + verify(exactly = 1) { cardinalRepository.findByCardinalNumber(40) } + verify(exactly = 0) { cardinalRepository.save(any()) } verify(exactly = 1) { accountRepository.save(any()) } } } diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt index 2da320fb..986f3b3e 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt @@ -15,7 +15,8 @@ import com.weeth.domain.file.domain.entity.File import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.file.domain.repository.FileRepository -import com.weeth.domain.user.domain.service.CardinalGetService +import com.weeth.domain.user.domain.entity.Cardinal +import com.weeth.domain.user.domain.repository.CardinalRepository import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.mockk.clearMocks @@ -31,7 +32,7 @@ class ManageReceiptUseCaseTest : val accountRepository = mockk() val fileReader = mockk() val fileRepository = mockk(relaxed = true) - val cardinalGetService = mockk(relaxUnitFun = true) + val cardinalRepository = mockk(relaxed = true) val fileMapper = mockk() val useCase = ManageReceiptUseCase( @@ -39,12 +40,16 @@ class ManageReceiptUseCaseTest : accountRepository, fileReader, fileRepository, - cardinalGetService, + cardinalRepository, fileMapper, ) beforeTest { - clearMocks(receiptRepository, accountRepository, fileReader, fileRepository, cardinalGetService, fileMapper) + clearMocks(receiptRepository, accountRepository, fileReader, fileRepository, cardinalRepository, fileMapper) + } + + fun stubExistingCardinal(cardinalNumber: Int) { + every { cardinalRepository.findByCardinalNumber(cardinalNumber) } returns Optional.of(mockk()) } describe("save") { @@ -53,7 +58,7 @@ class ManageReceiptUseCaseTest : val account = AccountTestFixture.createAccount(cardinal = 40) val savedReceipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) val files = listOf(mockk()) - val dto = + val request = ReceiptSaveRequest( "간식비", "편의점", @@ -63,12 +68,12 @@ class ManageReceiptUseCaseTest : listOf(FileSaveRequest("receipt.png", "TEMP/2024-09/receipt.png", 200L, "image/png")), ) - every { cardinalGetService.findByAdminSide(40) } returns mockk() + stubExistingCardinal(40) every { accountRepository.findByCardinal(40) } returns account every { receiptRepository.save(any()) } returns savedReceipt - every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, savedReceipt.id) } returns files + every { fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, savedReceipt.id) } returns files - useCase.save(dto) + useCase.save(request) verify(exactly = 1) { receiptRepository.save(any()) } verify(exactly = 1) { fileRepository.saveAll(files) } @@ -79,14 +84,14 @@ class ManageReceiptUseCaseTest : it("fileRepository.saveAll은 빈 리스트로 호출된다") { val account = AccountTestFixture.createAccount(cardinal = 40) val savedReceipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) - val dto = ReceiptSaveRequest("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) + val request = ReceiptSaveRequest("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) - every { cardinalGetService.findByAdminSide(40) } returns mockk() + stubExistingCardinal(40) every { accountRepository.findByCardinal(40) } returns account every { receiptRepository.save(any()) } returns savedReceipt every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, savedReceipt.id) } returns emptyList() - useCase.save(dto) + useCase.save(request) verify(exactly = 1) { receiptRepository.save(any()) } verify(exactly = 1) { fileRepository.saveAll(emptyList()) } @@ -95,12 +100,12 @@ class ManageReceiptUseCaseTest : context("존재하지 않는 기수로 저장 시") { it("AccountNotFoundException을 던진다") { - val dto = ReceiptSaveRequest("간식비", "편의점", 5_000, LocalDate.of(2024, 9, 1), 99, null) + val request = ReceiptSaveRequest("간식비", "편의점", 5_000, LocalDate.of(2024, 9, 1), 99, null) - every { cardinalGetService.findByAdminSide(99) } returns mockk() + stubExistingCardinal(99) every { accountRepository.findByCardinal(99) } returns null - shouldThrow { useCase.save(dto) } + shouldThrow { useCase.save(request) } } } } @@ -111,7 +116,7 @@ class ManageReceiptUseCaseTest : val account = AccountTestFixture.createAccount(cardinal = 40) val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) account.spend(Money.of(receipt.amount)) - val dto = + val request = ReceiptUpdateRequest( "desc", "source", @@ -123,14 +128,13 @@ class ManageReceiptUseCaseTest : val oldFiles = listOf(mockk()) val newFiles = listOf(mockk()) - every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() - every { accountRepository.findByCardinal(dto.cardinal) } returns account - + stubExistingCardinal(request.cardinal) + every { accountRepository.findByCardinal(request.cardinal) } returns account every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles - every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles + every { fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles - useCase.update(receiptId, dto) + useCase.update(receiptId, request) verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } verify(exactly = 1) { fileRepository.saveAll(newFiles) } @@ -141,13 +145,13 @@ class ManageReceiptUseCaseTest : val accountA = AccountTestFixture.createAccount(id = 1L, cardinal = 40) val accountB = AccountTestFixture.createAccount(id = 2L, cardinal = 41) val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = accountB) - val dto = ReceiptUpdateRequest("desc", "source", 2_000, LocalDate.of(2026, 1, 1), 40, null) + val request = ReceiptUpdateRequest("desc", "source", 2_000, LocalDate.of(2026, 1, 1), 40, null) - every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() - every { accountRepository.findByCardinal(dto.cardinal) } returns accountA + stubExistingCardinal(request.cardinal) + every { accountRepository.findByCardinal(request.cardinal) } returns accountA every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) - shouldThrow { useCase.update(receiptId, dto) } + shouldThrow { useCase.update(receiptId, request) } } it("빈 리스트로 업데이트 시 기존 파일을 모두 삭제한다") { @@ -155,7 +159,7 @@ class ManageReceiptUseCaseTest : val account = AccountTestFixture.createAccount(cardinal = 40) val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) account.spend(Money.of(receipt.amount)) - val dto = + val request = ReceiptUpdateRequest( "desc", "source", @@ -166,13 +170,13 @@ class ManageReceiptUseCaseTest : ) val oldFiles = listOf(mockk()) - every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() - every { accountRepository.findByCardinal(dto.cardinal) } returns account + stubExistingCardinal(request.cardinal) + every { accountRepository.findByCardinal(request.cardinal) } returns account every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, receiptId) } returns emptyList() - useCase.update(receiptId, dto) + useCase.update(receiptId, request) verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } verify(exactly = 1) { fileRepository.saveAll(emptyList()) } diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt index f737afe3..d9b6a8b6 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt @@ -1,14 +1,12 @@ package com.weeth.domain.attendance.application.mapper import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createActiveUser -import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createActiveUserWithAttendances -import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createAdminUserWithAttendances +import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createAdminUser import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createAttendance import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createOneDayMeeting import com.weeth.domain.attendance.fixture.AttendanceTestFixture.enrichUserProfile import com.weeth.domain.attendance.fixture.AttendanceTestFixture.setAttendanceId import com.weeth.domain.attendance.fixture.AttendanceTestFixture.setUserAttendanceStats -import com.weeth.domain.user.domain.entity.enums.Position import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull @@ -17,15 +15,14 @@ import java.time.LocalDate class AttendanceMapperTest : DescribeSpec({ - val mapper = AttendanceMapper() describe("toSummaryResponse") { it("사용자 + 당일 출석 객체를 MainResponse로 매핑한다") { val today = LocalDate.now() val meeting = createOneDayMeeting(today, 1, 1111, "Today") - val user = createActiveUserWithAttendances("이지훈", listOf(meeting)) - val attendance = user.attendances[0] + val user = createActiveUser("이지훈") + val attendance = createAttendance(meeting, user) val main = mapper.toSummaryResponse(user, attendance) @@ -52,8 +49,8 @@ class AttendanceMapperTest : it("일반 유저는 출석 코드가 null로 매핑된다") { val today = LocalDate.now() val meeting = createOneDayMeeting(today, 1, 1234, "Today") - val user = createActiveUserWithAttendances("일반유저", listOf(meeting)) - val attendance = user.attendances[0] + val user = createActiveUser("일반유저") + val attendance = createAttendance(meeting, user) val main = mapper.toSummaryResponse(user, attendance) @@ -67,8 +64,8 @@ class AttendanceMapperTest : val today = LocalDate.now() val expectedCode = 1234 val meeting = createOneDayMeeting(today, 1, expectedCode, "Today") - val adminUser = createAdminUserWithAttendances("관리자", listOf(meeting)) - val attendance = adminUser.attendances[0] + val adminUser = createAdminUser("관리자") + val attendance = createAttendance(meeting, adminUser) val main = mapper.toSummaryResponse(adminUser, attendance, isAdmin = true) @@ -123,7 +120,7 @@ class AttendanceMapperTest : it("Attendance를 InfoResponse로 매핑") { val meeting = createOneDayMeeting(LocalDate.now(), 1, 3333, "Info") val user = createActiveUser("유저B") - enrichUserProfile(user, Position.BE, "컴퓨터공학과", "20201234") + enrichUserProfile(user, "컴퓨터공학과", "20201234") val attendance = createAttendance(meeting, user) setAttendanceId(attendance, 10L) @@ -134,6 +131,7 @@ class AttendanceMapperTest : info.id shouldBe attendance.id info.status shouldBe attendance.status info.name shouldBe user.name + info.department shouldBe user.department } } }) diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt index 3e96b276..72fdd612 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt @@ -6,7 +6,7 @@ import com.weeth.domain.attendance.domain.entity.Attendance import com.weeth.domain.attendance.domain.enums.Status import com.weeth.domain.attendance.domain.repository.AttendanceRepository import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.mockk.every @@ -17,10 +17,10 @@ class CheckInAttendanceUseCaseTest : DescribeSpec({ val userId = 10L - val userGetService = mockk() + val userReader = mockk() val attendanceRepository = mockk() - val useCase = CheckInAttendanceUseCase(userGetService, attendanceRepository) + val useCase = CheckInAttendanceUseCase(userReader, attendanceRepository) describe("checkIn") { context("진행 중 정기모임이고 코드 일치하며 상태가 ATTEND가 아닐 때") { @@ -30,7 +30,7 @@ class CheckInAttendanceUseCaseTest : every { attendance.isWrong(1234) } returns false every { attendance.status } returns Status.PENDING - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns attendance every { user.attend() } returns Unit @@ -44,7 +44,7 @@ class CheckInAttendanceUseCaseTest : context("진행 중 정기모임이 없을 때") { it("AttendanceNotFoundException") { val user = mockk() - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns null shouldThrow { @@ -59,7 +59,7 @@ class CheckInAttendanceUseCaseTest : val attendance = mockk() every { attendance.isWrong(9999) } returns true - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns attendance shouldThrow { @@ -75,7 +75,7 @@ class CheckInAttendanceUseCaseTest : every { attendance.isWrong(1234) } returns false every { attendance.status } returns Status.ATTEND - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns attendance useCase.checkIn(userId, 1234) diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt index 6c600674..a3474d13 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt @@ -12,8 +12,8 @@ import com.weeth.domain.schedule.domain.entity.Meeting import com.weeth.domain.schedule.domain.service.MeetingGetService import com.weeth.domain.user.domain.entity.Cardinal import com.weeth.domain.user.domain.entity.enums.Status -import com.weeth.domain.user.domain.service.UserCardinalGetService -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.service.UserCardinalPolicy import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe import io.mockk.every @@ -23,16 +23,16 @@ import io.mockk.verify class GetAttendanceQueryServiceTest : DescribeSpec({ - val userGetService = mockk() - val userCardinalGetService = mockk() + val userReader = mockk() + val userCardinalPolicy = mockk() val meetingGetService = mockk() val attendanceRepository = mockk() val attendanceMapper = mockk() val queryService = GetAttendanceQueryService( - userGetService, - userCardinalGetService, + userReader, + userCardinalPolicy, meetingGetService, attendanceRepository, attendanceMapper, @@ -46,7 +46,7 @@ class GetAttendanceQueryServiceTest : val todayAttendance = mockk() val mapped = mockk() - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user every { attendanceRepository.findTodayByUserId(eq(userId), any(), any()) } returns todayAttendance every { attendanceMapper.toSummaryResponse(eq(user), eq(todayAttendance), eq(false)) } returns mapped @@ -60,7 +60,7 @@ class GetAttendanceQueryServiceTest : val user = createActiveUser("이지훈") val mapped = mockk() - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user every { attendanceRepository.findTodayByUserId(eq(userId), any(), any()) } returns null every { attendanceMapper.toSummaryResponse(user, null, false) } returns mapped @@ -77,10 +77,10 @@ class GetAttendanceQueryServiceTest : val attendance1 = mockk() val attendance2 = mockk() - every { userGetService.find(userId) } returns user + every { userReader.getById(userId) } returns user val currentCardinal = mockk() every { currentCardinal.cardinalNumber } returns 1 - every { userCardinalGetService.getCurrentCardinal(user) } returns currentCardinal + every { userCardinalPolicy.getCurrentCardinal(user) } returns currentCardinal every { attendanceRepository.findAllByUserIdAndCardinal(userId, 1) } returns listOf(attendance1, attendance2) val response1 = mockk() diff --git a/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt b/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt index f63aaff8..6744c1e2 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt @@ -44,17 +44,15 @@ class AttendanceRepositoryTest( meetingRepository.save(meeting) activeUser1 = - User - .builder() - .name("이지훈") - .status(Status.ACTIVE) - .build() + User( + name = "이지훈", + status = Status.ACTIVE, + ) activeUser2 = - User - .builder() - .name("이강혁") - .status(Status.ACTIVE) - .build() + User( + name = "이강혁", + status = Status.ACTIVE, + ) userRepository.saveAll(listOf(activeUser1, activeUser2)) activeUser1.accept() activeUser2.accept() diff --git a/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt b/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt index 5d8db16a..eec1deac 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt @@ -19,8 +19,8 @@ class AttendanceSaveServiceTest : val attendanceSaveService = AttendanceSaveService(attendanceRepository) describe("init") { - it("각 정기모임에 대한 Attendance 저장 후 user.add 호출") { - val user = mockk(relaxUnitFun = true) + it("각 정기모임에 대한 Attendance를 저장한다") { + val user = mockk() val meetingFirst = createMeeting() val meetingSecond = createMeeting() @@ -29,7 +29,6 @@ class AttendanceSaveServiceTest : attendanceSaveService.init(user, listOf(meetingFirst, meetingSecond)) verify(exactly = 2) { attendanceRepository.save(any()) } - verify(exactly = 2) { user.add(any()) } } } diff --git a/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt b/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt index 864321cf..53f73aab 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt @@ -3,55 +3,26 @@ package com.weeth.domain.attendance.fixture import com.weeth.domain.attendance.domain.entity.Attendance import com.weeth.domain.schedule.domain.entity.Meeting import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.entity.enums.Department -import com.weeth.domain.user.domain.entity.enums.Position import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.vo.AttendanceStats import org.springframework.test.util.ReflectionTestUtils import java.time.LocalDate import java.time.LocalDateTime object AttendanceTestFixture { fun createActiveUser(name: String): User = - User - .builder() - .name(name) - .status(Status.ACTIVE) - .build() + User( + name = name, + status = Status.ACTIVE, + ) fun createAdminUser(name: String): User = - User - .builder() - .name(name) - .status(Status.ACTIVE) - .role(Role.ADMIN) - .build() - - fun createActiveUserWithAttendances( - name: String, - meetings: List, - ): User { - val user = createActiveUser(name) - initAttendancesField(user) - meetings.forEach { meeting -> - val attendance = createAttendance(meeting, user) - user.add(attendance) - } - return user - } - - fun createAdminUserWithAttendances( - name: String, - meetings: List, - ): User { - val user = createAdminUser(name) - initAttendancesField(user) - meetings.forEach { meeting -> - val attendance = createAttendance(meeting, user) - user.add(attendance) - } - return user - } + User( + name = name, + status = Status.ACTIVE, + role = Role.ADMIN, + ) fun createAttendance( meeting: Meeting, @@ -101,36 +72,23 @@ object AttendanceTestFixture { attendanceCount: Int, absenceCount: Int, ) { - ReflectionTestUtils.setField(user, "attendanceCount", attendanceCount) - ReflectionTestUtils.setField(user, "absenceCount", absenceCount) - } - - fun enrichUserProfile( - user: User, - position: Position, - department: Department, - studentId: String, - ) { - ReflectionTestUtils.setField(user, "position", position) - ReflectionTestUtils.setField(user, "department", department) - ReflectionTestUtils.setField(user, "studentId", studentId) + ReflectionTestUtils.setField( + user, + "attendanceStats", + AttendanceStats( + attendanceCount = attendanceCount, + absenceCount = absenceCount, + attendanceRate = if (attendanceCount + absenceCount > 0) (attendanceCount * 100) / (attendanceCount + absenceCount) else 0, + ), + ) } fun enrichUserProfile( user: User, - position: Position, - departmentKoreanValue: String, + department: String, studentId: String, ) { - ReflectionTestUtils.setField(user, "position", position) - val department = Department.to(departmentKoreanValue) ReflectionTestUtils.setField(user, "department", department) ReflectionTestUtils.setField(user, "studentId", studentId) } - - private fun initAttendancesField(user: User) { - if (user.attendances == null) { - ReflectionTestUtils.setField(user, "attendances", mutableListOf()) - } - } } diff --git a/src/test/kotlin/com/weeth/domain/board/application/mapper/PostMapperTest.kt b/src/test/kotlin/com/weeth/domain/board/application/mapper/PostMapperTest.kt index dda26cba..59718b59 100644 --- a/src/test/kotlin/com/weeth/domain/board/application/mapper/PostMapperTest.kt +++ b/src/test/kotlin/com/weeth/domain/board/application/mapper/PostMapperTest.kt @@ -1,12 +1,10 @@ package com.weeth.domain.board.application.mapper import com.weeth.domain.board.domain.entity.Post -import com.weeth.domain.board.domain.entity.enums.BoardType import com.weeth.domain.comment.application.dto.response.CommentResponse import com.weeth.domain.file.application.dto.response.FileResponse import com.weeth.domain.file.domain.entity.FileStatus import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.entity.enums.Position import com.weeth.domain.user.domain.entity.enums.Role import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -22,7 +20,6 @@ class PostMapperTest : val post = mockk() every { user.name } returns "테스터" - every { user.position } returns Position.BE every { user.role } returns Role.USER every { post.id } returns 1L @@ -50,7 +47,6 @@ class PostMapperTest : CommentResponse( id = 10L, name = "댓글작성자", - position = Position.BE, role = Role.USER, content = "댓글", time = LocalDateTime.now(), diff --git a/src/test/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCaseTest.kt index 9374e211..a403a5e8 100644 --- a/src/test/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/board/application/usecase/command/ManagePostUseCaseTest.kt @@ -21,7 +21,7 @@ import com.weeth.domain.file.domain.repository.FileRepository import com.weeth.domain.user.domain.entity.User import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.domain.entity.enums.Status -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader import com.weeth.domain.user.fixture.UserTestFixture import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec @@ -37,7 +37,7 @@ class ManagePostUseCaseTest : DescribeSpec({ val postRepository = mockk() val boardRepository = mockk() - val userGetService = mockk() + val userReader = mockk() val fileRepository = mockk() val fileReader = mockk() val fileMapper = mockk() @@ -47,7 +47,7 @@ class ManagePostUseCaseTest : ManagePostUseCase( postRepository, boardRepository, - userGetService, + userReader, fileRepository, fileReader, fileMapper, @@ -71,17 +71,16 @@ class ManagePostUseCaseTest : id: Long = 1L, role: Role = Role.USER, ): User = - User - .builder() - .id(id) - .name("적순") - .email("test1@test.com") - .status(Status.ACTIVE) - .role(role) - .build() + User( + id = id, + name = "적순", + email = "test1@test.com", + status = Status.ACTIVE, + role = role, + ) beforeTest { - clearMocks(postRepository, boardRepository, userGetService, fileRepository, fileReader, fileMapper, postMapper) + clearMocks(postRepository, boardRepository, userReader, fileRepository, fileReader, fileMapper, postMapper) every { postRepository.save(any()) } answers { firstArg() } every { fileMapper.toFileList(any(), any(), any()) } returns emptyList() every { fileRepository.saveAll(any>()) } returns emptyList() @@ -96,7 +95,7 @@ class ManagePostUseCaseTest : val board = Board(id = 10L, name = "일반", type = BoardType.GENERAL) val request = CreatePostRequest(title = "제목", content = "내용") - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { boardRepository.findByIdAndIsDeletedFalse(10L) } returns board val result = useCase.save(10L, request, 1L) @@ -116,7 +115,7 @@ class ManagePostUseCaseTest : ) val request = CreatePostRequest(title = "제목", content = "내용") - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { boardRepository.findByIdAndIsDeletedFalse(20L) } returns board shouldThrow { @@ -137,7 +136,7 @@ class ManagePostUseCaseTest : ) val request = CreatePostRequest(title = "제목", content = "내용") - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { boardRepository.findByIdAndIsDeletedFalse(21L) } returns board shouldThrow { @@ -157,7 +156,7 @@ class ManagePostUseCaseTest : cardinalNumber = 6, ) - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { boardRepository.findByIdAndIsDeletedFalse(11L) } returns board useCase.save(11L, request, 1L) @@ -175,7 +174,7 @@ class ManagePostUseCaseTest : val user = createUser(1L, Role.USER) val request = CreatePostRequest(title = "제목", content = "내용") - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { boardRepository.findByIdAndIsDeletedFalse(999L) } returns null shouldThrow { diff --git a/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/CommentConcurrencyTest.kt b/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/CommentConcurrencyTest.kt index 7c718db8..52155f40 100644 --- a/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/CommentConcurrencyTest.kt +++ b/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/CommentConcurrencyTest.kt @@ -66,12 +66,11 @@ class CommentConcurrencyTest( fun createUsers(size: Int): List = (1..size).map { i -> userRepository.save( - User - .builder() - .name("user$i") - .email("user$i@test.com") - .status(Status.ACTIVE) - .build(), + User( + name = "user$i", + email = "user$i@test.com", + status = Status.ACTIVE, + ), ) } diff --git a/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCaseTest.kt index 008c6ba8..e214c1c9 100644 --- a/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/comment/application/usecase/command/ManageCommentUseCaseTest.kt @@ -16,7 +16,7 @@ import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.entity.FileStatus import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.file.domain.repository.FileRepository -import com.weeth.domain.user.domain.service.UserGetService +import com.weeth.domain.user.domain.repository.UserReader import com.weeth.domain.user.fixture.UserTestFixture import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec @@ -32,7 +32,7 @@ class ManageCommentUseCaseTest : DescribeSpec({ val commentRepository = mockk(relaxUnitFun = true) val postRepository = mockk() - val userGetService = mockk() + val userReader = mockk() val fileReader = mockk() val fileRepository = mockk(relaxed = true) val fileMapper = mockk() @@ -41,14 +41,14 @@ class ManageCommentUseCaseTest : ManageCommentUseCase( commentRepository, postRepository, - userGetService, + userReader, fileReader, fileRepository, fileMapper, ) beforeTest { - clearMocks(commentRepository, postRepository, userGetService, fileReader, fileRepository, fileMapper) + clearMocks(commentRepository, postRepository, userReader, fileReader, fileRepository, fileMapper) every { fileMapper.toFileList(any(), FileOwnerType.COMMENT, any()) } returns emptyList() every { commentRepository.save(any()) } answers { firstArg() } every { fileReader.findAll(FileOwnerType.COMMENT, any(), any()) } returns emptyList() @@ -61,7 +61,7 @@ class ManageCommentUseCaseTest : val post = PostTestFixture.create(id = 10L, user = user) val dto = CommentSaveRequest(parentCommentId = null, content = "최상위 댓글", files = null) - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { postRepository.findByIdWithLock(10L) } returns post useCase.savePostComment(dto, postId = 10L, userId = 1L) @@ -76,7 +76,7 @@ class ManageCommentUseCaseTest : val post = PostTestFixture.create(id = 10L, user = user) val dto = CommentSaveRequest(parentCommentId = 999L, content = "대댓글", files = null) - every { userGetService.find(1L) } returns user + every { userReader.getById(1L) } returns user every { postRepository.findByIdWithLock(10L) } returns post every { commentRepository.findByIdAndPostId(999L, 10L) } returns null diff --git a/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/CommentQueryPerformanceTest.kt b/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/CommentQueryPerformanceTest.kt index fc84aa54..501d986a 100644 --- a/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/CommentQueryPerformanceTest.kt +++ b/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/CommentQueryPerformanceTest.kt @@ -17,7 +17,6 @@ import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.port.FileAccessUrlPort import com.weeth.domain.file.domain.repository.FileRepository import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.entity.enums.Position import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.domain.entity.enums.Status import com.weeth.domain.user.domain.repository.UserRepository @@ -47,14 +46,13 @@ class CommentQueryPerformanceTest( fun createUser(): User = userRepository.save( - User - .builder() - .name("perf-user") - .email("perf-user@test.com") - .status(Status.ACTIVE) - .position(Position.BE) - .role(Role.USER) - .build(), + User( + name = "perf-user", + email = "perf-user@test.com", + department = "컴퓨터공학과", + status = Status.ACTIVE, + role = Role.USER, + ), ) fun createBoard(): Board = diff --git a/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/GetCommentQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/GetCommentQueryServiceTest.kt index 023faf4c..7d0e3213 100644 --- a/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/GetCommentQueryServiceTest.kt +++ b/src/test/kotlin/com/weeth/domain/comment/application/usecase/query/GetCommentQueryServiceTest.kt @@ -7,7 +7,6 @@ import com.weeth.domain.comment.fixture.CommentTestFixture import com.weeth.domain.file.application.mapper.FileMapper import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.repository.FileReader -import com.weeth.domain.user.domain.entity.enums.Position import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.fixture.UserTestFixture import io.kotest.core.spec.style.DescribeSpec @@ -38,7 +37,6 @@ class GetCommentQueryServiceTest : ) = CommentResponse( id = id, name = "테스트유저", - position = Position.BE, role = Role.USER, content = "content", time = LocalDateTime.now(), diff --git a/src/test/kotlin/com/weeth/domain/user/application/usecase/UserManageUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/user/application/usecase/UserManageUseCaseTest.kt deleted file mode 100644 index 680b0e94..00000000 --- a/src/test/kotlin/com/weeth/domain/user/application/usecase/UserManageUseCaseTest.kt +++ /dev/null @@ -1,238 +0,0 @@ -package com.weeth.domain.user.application.usecase - -import com.weeth.domain.attendance.domain.service.AttendanceSaveService -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.schedule.domain.service.MeetingGetService -import com.weeth.domain.user.application.dto.request.UserRequestDto -import com.weeth.domain.user.application.dto.response.UserResponseDto -import com.weeth.domain.user.application.exception.InvalidUserOrderException -import com.weeth.domain.user.application.mapper.UserMapper -import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.entity.UserCardinal -import com.weeth.domain.user.domain.entity.enums.Role -import com.weeth.domain.user.domain.entity.enums.Status -import com.weeth.domain.user.domain.entity.enums.UsersOrderBy -import com.weeth.domain.user.domain.service.CardinalGetService -import com.weeth.domain.user.domain.service.UserCardinalGetService -import com.weeth.domain.user.domain.service.UserCardinalSaveService -import com.weeth.domain.user.domain.service.UserDeleteService -import com.weeth.domain.user.domain.service.UserGetService -import com.weeth.domain.user.domain.service.UserUpdateService -import com.weeth.domain.user.fixture.CardinalTestFixture -import com.weeth.domain.user.fixture.UserTestFixture -import com.weeth.global.auth.jwt.domain.port.RefreshTokenStorePort -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.springframework.security.crypto.password.PasswordEncoder -import java.time.LocalDateTime -import java.util.ArrayList - -class UserManageUseCaseTest : - DescribeSpec({ - - val userGetService = mockk() - val userUpdateService = mockk(relaxUnitFun = true) - val userDeleteService = mockk(relaxUnitFun = true) - val attendanceSaveService = mockk(relaxUnitFun = true) - val meetingGetService = mockk() - val refreshTokenStorePort = mockk(relaxUnitFun = true) - val cardinalGetService = mockk() - val userCardinalSaveService = mockk(relaxUnitFun = true) - val userCardinalGetService = mockk() - val userMapper = mockk() - val passwordEncoder = mockk() - - val useCase = - UserManageUseCaseImpl( - userGetService, - userUpdateService, - userDeleteService, - attendanceSaveService, - meetingGetService, - refreshTokenStorePort, - cardinalGetService, - userCardinalSaveService, - userCardinalGetService, - userMapper, - passwordEncoder, - ) - - describe("findAllByAdmin") { - context("orderBy가 null이면") { - it("예외가 발생한다") { - shouldThrow { - useCase.findAllByAdmin(null) - } - } - } - - context("orderBy에 맞게 정렬하여 조회할 때") { - it("정렬된 결과를 반환한다") { - val user1 = UserTestFixture.createActiveUser1() - val user2 = UserTestFixture.createWaitingUser2() - val cd1 = CardinalTestFixture.createCardinal(id = 1L, cardinalNumber = 6, year = 2020, semester = 2) - val cd2 = CardinalTestFixture.createCardinal(id = 2L, cardinalNumber = 7, year = 2021, semester = 1) - val uc1 = UserCardinal(user1, cd1) - val uc2 = UserCardinal(user2, cd2) - - val adminResponse1 = - UserResponseDto.AdminResponse( - 1, - "aaa", - "a@a.com", - "202034420", - "01011112222", - "산업공학과", - listOf(6), - null, - Status.ACTIVE, - null, - 0, - 0, - 0, - 0, - 0, - LocalDateTime.now().minusDays(3), - LocalDateTime.now(), - ) - val adminResponse2 = - UserResponseDto.AdminResponse( - 2, - "bbb", - "b@b.com", - "202045678", - "01033334444", - "컴퓨터공학과", - listOf(7), - null, - Status.WAITING, - null, - 0, - 0, - 0, - 0, - 0, - LocalDateTime.now().minusDays(2), - LocalDateTime.now(), - ) - - every { userCardinalGetService.getUserCardinals(user1) } returns listOf(uc1) - every { userCardinalGetService.getUserCardinals(user2) } returns listOf(uc2) - every { userCardinalGetService.findAll() } returns listOf(uc2, uc1) - every { userMapper.toAdminResponse(user1, listOf(uc1)) } returns adminResponse1 - every { userMapper.toAdminResponse(user2, listOf(uc2)) } returns adminResponse2 - - val result = useCase.findAllByAdmin(UsersOrderBy.NAME_ASCENDING) - - result shouldHaveSize 2 - result[0].name() shouldBe "aaa" - result[1].name() shouldBe "bbb" - } - } - } - - describe("accept") { - it("비활성유저 승인시 출석초기화가 정상 호출된다") { - val user1 = UserTestFixture.createWaitingUser1(1L) - val userIds = UserRequestDto.UserId(listOf(1L)) - val cardinal = CardinalTestFixture.createCardinal(id = 1L, cardinalNumber = 8, year = 2020, semester = 2) - val meetings = listOf(mockk()) - - every { userGetService.findAll(userIds.userId()) } returns listOf(user1) - every { userCardinalGetService.getCurrentCardinal(user1) } returns cardinal - every { meetingGetService.find(8) } returns meetings - - useCase.accept(userIds) - - verify { userUpdateService.accept(user1) } - verify { attendanceSaveService.init(user1, meetings) } - } - } - - describe("update") { - it("유저권한변경시 DB와 Redis 모두 갱신된다") { - val user1 = UserTestFixture.createActiveUser1(1L) - val request = UserRequestDto.UserRoleUpdate(1L, Role.ADMIN) - - every { userGetService.find(1L) } returns user1 - - useCase.update(listOf(request)) - - verify { userUpdateService.update(user1, "ADMIN") } - verify { refreshTokenStorePort.updateRole(1L, Role.ADMIN) } - } - } - - describe("leave") { - it("회원탈퇴시 토큰무효화 및 유저상태변경된다") { - val user1 = UserTestFixture.createActiveUser1(1L) - every { userGetService.find(1L) } returns user1 - - useCase.leave(1L) - - verify { refreshTokenStorePort.delete(1L) } - verify { userDeleteService.leave(user1) } - } - } - - describe("ban") { - it("회원ban시 토큰무효화 및 유저상태변경된다") { - val user1 = UserTestFixture.createActiveUser1(1L) - val ids = UserRequestDto.UserId(listOf(1L)) - every { userGetService.findAll(ids.userId()) } returns listOf(user1) - - useCase.ban(ids) - - verify { refreshTokenStorePort.delete(1L) } - verify { userDeleteService.ban(user1) } - } - } - - describe("applyOB") { - it("현재기수 OB신청시 출석초기화 및 기수업데이트된다") { - val user = - User - .builder() - .id(1L) - .name("aaa") - .status(Status.ACTIVE) - .attendances(ArrayList()) - .build() - val nextCardinal = CardinalTestFixture.createCardinal(id = 1L, cardinalNumber = 4, year = 2020, semester = 2) - val request = UserRequestDto.UserApplyOB(1L, 4) - val meeting = listOf(mockk()) - - every { userGetService.find(1L) } returns user - every { cardinalGetService.findByAdminSide(4) } returns nextCardinal - every { userCardinalGetService.notContains(user, nextCardinal) } returns true - every { userCardinalGetService.isCurrent(user, nextCardinal) } returns true - every { meetingGetService.find(4) } returns meeting - - useCase.applyOB(listOf(request)) - - verify { attendanceSaveService.init(user, meeting) } - verify { userCardinalSaveService.save(any()) } - } - } - - describe("reset") { - it("비밀번호초기화시 모든유저에 reset이 호출된다") { - val user1 = UserTestFixture.createActiveUser1(1L) - val user2 = UserTestFixture.createActiveUser2(2L) - val ids = UserRequestDto.UserId(listOf(1L, 2L)) - - every { userGetService.findAll(ids.userId()) } returns listOf(user1, user2) - - useCase.reset(ids) - - verify { userGetService.findAll(ids.userId()) } - verify { userUpdateService.reset(user1, passwordEncoder) } - verify { userUpdateService.reset(user2, passwordEncoder) } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt new file mode 100644 index 00000000..f4c88e7a --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt @@ -0,0 +1,158 @@ +package com.weeth.domain.user.application.usecase.command + +import com.weeth.domain.attendance.domain.service.AttendanceSaveService +import com.weeth.domain.schedule.domain.entity.Meeting +import com.weeth.domain.schedule.domain.service.MeetingGetService +import com.weeth.domain.user.application.dto.request.UserApplyObRequest +import com.weeth.domain.user.application.dto.request.UserIdsRequest +import com.weeth.domain.user.application.dto.request.UserRoleUpdateRequest +import com.weeth.domain.user.domain.entity.UserCardinal +import com.weeth.domain.user.domain.entity.enums.Role +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.repository.CardinalRepository +import com.weeth.domain.user.domain.repository.UserCardinalRepository +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.service.UserCardinalPolicy +import com.weeth.domain.user.fixture.CardinalTestFixture +import com.weeth.domain.user.fixture.UserTestFixture +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class AdminUserUseCaseTest : + DescribeSpec({ + val userReader = mockk() + val attendanceSaveService = mockk(relaxUnitFun = true) + val meetingGetService = mockk() + val cardinalRepository = mockk() + val userCardinalRepository = mockk(relaxUnitFun = true) + val userCardinalPolicy = mockk() + + val useCase = + AdminUserUseCase( + userReader, + attendanceSaveService, + meetingGetService, + cardinalRepository, + userCardinalRepository, + userCardinalPolicy, + ) + + beforeTest { + clearMocks( + userReader, + attendanceSaveService, + meetingGetService, + cardinalRepository, + userCardinalRepository, + userCardinalPolicy, + ) + } + + describe("accept") { + it("비활성 유저 승인 시 출석 초기화를 수행한다") { + val user = UserTestFixture.createWaitingUser1(1L) + val currentCardinal = CardinalTestFixture.createCardinal(id = 1L, cardinalNumber = 8, year = 2025, semester = 1) + val meetings = listOf(mockk()) + + every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) + every { userCardinalPolicy.getCurrentCardinal(user) } returns currentCardinal + every { meetingGetService.find(8) } returns meetings + + useCase.accept(UserIdsRequest(listOf(1L))) + + verify(exactly = 1) { attendanceSaveService.init(user, meetings) } + user.status shouldBe Status.ACTIVE + } + } + + describe("updateRole") { + it("권한 변경 시 엔티티 권한을 갱신한다") { + val user = UserTestFixture.createActiveUser1(1L) + every { userReader.getById(1L) } returns user + + useCase.updateRole(listOf(UserRoleUpdateRequest(1L, Role.ADMIN))) + + user.role shouldBe Role.ADMIN + } + } + + describe("ban") { + it("회원 추방 시 상태를 BANNED로 변경한다") { + val user = UserTestFixture.createActiveUser1(1L) + every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) + + useCase.ban(UserIdsRequest(listOf(1L))) + + user.status shouldBe Status.BANNED + } + } + + describe("applyOb") { + it("다음 기수로 OB 신청 시 출석을 초기화하고 user-cardinal을 저장한다") { + val user = UserTestFixture.createActiveUser1(1L) + val currentCardinal = CardinalTestFixture.createCardinal(id = 10L, cardinalNumber = 3, year = 2024, semester = 2) + val nextCardinal = CardinalTestFixture.createCardinal(id = 11L, cardinalNumber = 4, year = 2025, semester = 1) + val meetings = listOf(mockk()) + val request = listOf(UserApplyObRequest(1L, 4)) + + every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) + every { userCardinalRepository.findAllByUsers(listOf(user)) } returns listOf(UserCardinal(user, currentCardinal)) + every { cardinalRepository.findAllByCardinalNumberIn(listOf(4)) } returns listOf(nextCardinal) + every { meetingGetService.findByCardinals(listOf(4)) } returns mapOf(4 to meetings) + every { userCardinalRepository.save(any()) } answers { firstArg() } + + useCase.applyOb(request) + + verify(exactly = 1) { attendanceSaveService.init(user, meetings) } + verify(exactly = 1) { userCardinalRepository.save(match { it.user == user && it.cardinal == nextCardinal }) } + } + + it("이미 해당 기수를 보유한 유저는 저장을 스킵한다") { + val user = UserTestFixture.createActiveUser1(1L) + val cardinal = CardinalTestFixture.createCardinal(id = 11L, cardinalNumber = 4, year = 2025, semester = 1) + val request = listOf(UserApplyObRequest(1L, 4)) + + every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) + every { userCardinalRepository.findAllByUsers(listOf(user)) } returns listOf(UserCardinal(user, cardinal)) + every { cardinalRepository.findAllByCardinalNumberIn(listOf(4)) } returns listOf(cardinal) + + useCase.applyOb(request) + + verify(exactly = 0) { meetingGetService.findByCardinals(any()) } + verify(exactly = 0) { userCardinalRepository.save(any()) } + verify(exactly = 0) { attendanceSaveService.init(any(), any()) } + } + + it("요청 목록이 비어 있으면 아무 처리도 하지 않는다") { + useCase.applyOb(emptyList()) + + verify(exactly = 0) { userReader.findAllByIds(any()) } + verify(exactly = 0) { userCardinalRepository.save(any()) } + } + + it("존재하지 않는 기수라면 새로 생성한다") { + val user = UserTestFixture.createActiveUser1(1L) + val currentCardinal = CardinalTestFixture.createCardinal(id = 10L, cardinalNumber = 3, year = 2024, semester = 2) + val createdCardinal = CardinalTestFixture.createCardinal(id = 12L, cardinalNumber = 5, year = 2025, semester = 2) + val meetings = listOf(mockk()) + val request = listOf(UserApplyObRequest(1L, 5)) + + every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) + every { userCardinalRepository.findAllByUsers(listOf(user)) } returns listOf(UserCardinal(user, currentCardinal)) + every { cardinalRepository.findAllByCardinalNumberIn(listOf(5)) } returns emptyList() + every { cardinalRepository.save(any()) } returns createdCardinal + every { meetingGetService.findByCardinals(listOf(5)) } returns mapOf(5 to meetings) + every { userCardinalRepository.save(any()) } answers { firstArg() } + + useCase.applyOb(request) + + verify(exactly = 1) { cardinalRepository.save(any()) } + verify(exactly = 1) { attendanceSaveService.init(user, meetings) } + verify(exactly = 1) { userCardinalRepository.save(match { it.user == user && it.cardinal == createdCardinal }) } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AuthUserUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AuthUserUseCaseTest.kt new file mode 100644 index 00000000..1d7f89af --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AuthUserUseCaseTest.kt @@ -0,0 +1,266 @@ +package com.weeth.domain.user.application.usecase.command + +import com.weeth.domain.user.application.dto.request.SignUpRequest +import com.weeth.domain.user.application.dto.request.SocialLoginRequest +import com.weeth.domain.user.application.dto.request.UpdateUserProfileRequest +import com.weeth.domain.user.application.exception.StudentIdExistsException +import com.weeth.domain.user.application.exception.UserInActiveException +import com.weeth.domain.user.application.mapper.UserMapper +import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.UserCardinal +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalRepository +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.repository.UserRepository +import com.weeth.domain.user.domain.repository.UserSocialAccountRepository +import com.weeth.domain.user.fixture.CardinalTestFixture +import com.weeth.domain.user.fixture.UserTestFixture +import com.weeth.global.auth.apple.AppleAuthService +import com.weeth.global.auth.apple.dto.AppleTokenResponse +import com.weeth.global.auth.apple.dto.AppleUserInfo +import com.weeth.global.auth.jwt.application.dto.JwtDto +import com.weeth.global.auth.jwt.application.service.JwtTokenExtractor +import com.weeth.global.auth.jwt.application.usecase.JwtManageUseCase +import com.weeth.global.auth.kakao.KakaoAuthService +import com.weeth.global.auth.kakao.dto.KakaoAccount +import com.weeth.global.auth.kakao.dto.KakaoProfile +import com.weeth.global.auth.kakao.dto.KakaoTokenResponse +import com.weeth.global.auth.kakao.dto.KakaoUserInfoResponse +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import jakarta.servlet.http.HttpServletRequest +import java.util.Optional + +class AuthUserUseCaseTest : + DescribeSpec({ + val userRepository = mockk(relaxed = true) + val userReader = mockk() + val cardinalReader = mockk() + val userCardinalRepository = mockk(relaxed = true) + val userSocialAccountRepository = mockk(relaxed = true) + val mapper = mockk() + val kakaoAuthService = mockk() + val appleAuthService = mockk() + val jwtManageUseCase = mockk() + val jwtTokenExtractor = mockk() + + val useCase = + AuthUserUseCase( + userRepository, + userReader, + cardinalReader, + userCardinalRepository, + mapper, + userSocialAccountRepository, + kakaoAuthService, + appleAuthService, + jwtManageUseCase, + jwtTokenExtractor, + ) + + describe("apply") { + it("유저와 유저-기수 연관관계를 저장한다") { + val request = SignUpRequest("홍길동", "a@test.com", "20201234", "01012345678", "컴퓨터공학과", 7) + val user = UserTestFixture.createActiveUser1(1L) + val cardinal = CardinalTestFixture.createCardinal(id = 10L, cardinalNumber = 7, year = 2025, semester = 1) + + every { userRepository.existsByStudentId(request.studentId) } returns false + every { userRepository.existsByTelValue(request.tel) } returns false + every { cardinalReader.getByCardinalNumber(request.cardinal) } returns cardinal + every { mapper.toEntity(request) } returns user + every { userRepository.save(user) } returns user + every { userCardinalRepository.save(any()) } answers { firstArg() } + + useCase.apply(request) + + verify(exactly = 1) { userRepository.save(user) } + verify(exactly = 1) { userCardinalRepository.save(any()) } + } + + it("학번 중복이면 StudentIdExistsException") { + val request = SignUpRequest("홍길동", "a@test.com", "20201234", "01012345678", "컴퓨터공학과", 7) + every { userRepository.existsByStudentId(request.studentId) } returns true + + shouldThrow { + useCase.apply(request) + } + } + } + + describe("updateProfile") { + it("내 정보를 수정한다") { + val user = UserTestFixture.createActiveUser1(1L) + val request = UpdateUserProfileRequest("변경이름", "new@test.com", "20209999", "01099998888", "경영학과") + + every { userRepository.existsByStudentIdAndIdIsNot(request.studentId, 1L) } returns false + every { userRepository.existsByTelAndIdIsNotValue(request.tel, 1L) } returns false + every { userReader.getById(1L) } returns user + + useCase.updateProfile(request, 1L) + + user.name shouldBe "변경이름" + user.department shouldBe "경영학과" + } + } + + describe("leave") { + it("회원 탈퇴 시 상태를 LEFT로 변경한다") { + val user = UserTestFixture.createActiveUser1(1L) + every { userReader.getById(1L) } returns user + + useCase.leave(1L) + + user.status shouldBe Status.LEFT + } + } + + describe("socialLoginByKakao") { + it("가입된 활성 사용자면 토큰을 발급한다") { + val request = SocialLoginRequest("auth-code") + val tokenResponse = KakaoTokenResponse("bearer", "kakao-access", 3600, "kakao-refresh", 3600) + val userInfo = + KakaoUserInfoResponse( + id = 1L, + kakaoAccount = KakaoAccount(isEmailValid = true, isEmailVerified = true, email = "a@test.com"), + ) + val user = UserTestFixture.createActiveUser1(1L) + + every { kakaoAuthService.getKakaoToken("auth-code") } returns tokenResponse + every { kakaoAuthService.getUserInfo("kakao-access") } returns userInfo + every { userSocialAccountRepository.findByProviderAndProviderUserId(any(), any()) } returns Optional.empty() + every { userRepository.findByEmailValue("a@test.com") } returns Optional.of(user) + every { userSocialAccountRepository.save(any()) } answers { firstArg() } + every { jwtManageUseCase.create(user.id, user.emailValue, user.role) } returns JwtDto("access", "refresh") + + val result = useCase.socialLoginByKakao(request) + + result.isNewUser shouldBe false + result.profileCompleted shouldBe false + result.accessToken shouldBe "access" + result.refreshToken shouldBe "refresh" + } + + it("기존 사용자가 추가 프로필 payload 없이 로그인하면 provider 이름으로 덮어쓰지 않는다") { + val request = SocialLoginRequest("auth-code") + val tokenResponse = KakaoTokenResponse("bearer", "kakao-access", 3600, "kakao-refresh", 3600) + val userInfo = + KakaoUserInfoResponse( + id = 1L, + kakaoAccount = + KakaoAccount( + isEmailValid = true, + isEmailVerified = true, + email = "a@test.com", + profile = KakaoProfile(nickname = "카카오닉네임"), + ), + ) + val user = UserTestFixture.createActiveUser1(1L).also { it.name = "내가수정한이름" } + + every { kakaoAuthService.getKakaoToken("auth-code") } returns tokenResponse + every { kakaoAuthService.getUserInfo("kakao-access") } returns userInfo + every { userSocialAccountRepository.findByProviderAndProviderUserId(any(), any()) } returns Optional.empty() + every { userRepository.findByEmailValue("a@test.com") } returns Optional.of(user) + every { userSocialAccountRepository.save(any()) } answers { firstArg() } + every { jwtManageUseCase.create(user.id, user.emailValue, user.role) } returns JwtDto("access", "refresh") + + useCase.socialLoginByKakao(request) + + user.name shouldBe "내가수정한이름" + } + + it("식별자가 없고 이메일 사용자도 없으면 사용자를 생성하고 로그인한다") { + val request = SocialLoginRequest("auth-code") + val tokenResponse = KakaoTokenResponse("bearer", "kakao-access", 3600, "kakao-refresh", 3600) + val userInfo = + KakaoUserInfoResponse( + id = 1L, + kakaoAccount = KakaoAccount(isEmailValid = true, isEmailVerified = true, email = "new@test.com"), + ) + val createdUser = User.create(name = "", email = "new@test.com", studentId = "", tel = "", department = "") + + every { kakaoAuthService.getKakaoToken("auth-code") } returns tokenResponse + every { kakaoAuthService.getUserInfo("kakao-access") } returns userInfo + every { userSocialAccountRepository.findByProviderAndProviderUserId(any(), any()) } returns Optional.empty() + every { userRepository.findByEmailValue("new@test.com") } returns Optional.empty() + every { userRepository.save(any()) } returns createdUser + every { userSocialAccountRepository.save(any()) } answers { firstArg() } + every { + jwtManageUseCase.create( + createdUser.id, + createdUser.emailValue, + createdUser.role, + ) + } returns + JwtDto( + "access", + "refresh", + ) + + val result = useCase.socialLoginByKakao(request) + + result.isNewUser shouldBe true + result.email shouldBe "new@test.com" + result.accessToken shouldBe "access" + } + + it("추방된 사용자면 예외를 던진다") { + val request = SocialLoginRequest("auth-code") + val tokenResponse = KakaoTokenResponse("bearer", "kakao-access", 3600, "kakao-refresh", 3600) + val userInfo = + KakaoUserInfoResponse( + id = 1L, + kakaoAccount = KakaoAccount(isEmailValid = true, isEmailVerified = true, email = "ban@test.com"), + ) + val bannedUser = UserTestFixture.createActiveUser1(1L).also { it.ban() } + + every { kakaoAuthService.getKakaoToken("auth-code") } returns tokenResponse + every { kakaoAuthService.getUserInfo("kakao-access") } returns userInfo + every { userSocialAccountRepository.findByProviderAndProviderUserId(any(), any()) } returns Optional.empty() + every { userRepository.findByEmailValue("ban@test.com") } returns Optional.of(bannedUser) + every { userSocialAccountRepository.save(any()) } answers { firstArg() } + + shouldThrow { + useCase.socialLoginByKakao(request) + } + } + } + + describe("socialLoginByApple") { + it("가입된 활성 사용자면 토큰을 발급한다") { + val request = SocialLoginRequest("apple-code") + val tokenResponse = AppleTokenResponse("apple-access", "bearer", 3600, "apple-refresh", "id-token") + val userInfo = AppleUserInfo(appleId = "apple-sub", email = "apple@test.com", emailVerified = true) + val user = UserTestFixture.createActiveUser1(1L) + + every { appleAuthService.getAppleToken("apple-code") } returns tokenResponse + every { appleAuthService.verifyAndDecodeIdToken("id-token") } returns userInfo + every { userSocialAccountRepository.findByProviderAndProviderUserId(any(), any()) } returns Optional.empty() + every { userRepository.findByEmailValue("apple@test.com") } returns Optional.of(user) + every { userSocialAccountRepository.save(any()) } answers { firstArg() } + every { jwtManageUseCase.create(user.id, user.emailValue, user.role) } returns JwtDto("access", "refresh") + + val result = useCase.socialLoginByApple(request) + + result.isNewUser shouldBe false + result.accessToken shouldBe "access" + } + } + + describe("refreshToken") { + it("헤더의 리프레시 토큰으로 토큰을 재발급한다") { + val servletRequest = mockk() + every { jwtTokenExtractor.extractRefreshToken(servletRequest) } returns "refresh-token" + every { jwtManageUseCase.reIssueToken("refresh-token") } returns JwtDto("new-access", "new-refresh") + + val result = useCase.refreshToken(servletRequest) + + result.accessToken shouldBe "new-access" + result.refreshToken shouldBe "new-refresh" + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/user/application/usecase/CardinalUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/CardinalUseCaseTest.kt similarity index 56% rename from src/test/kotlin/com/weeth/domain/user/application/usecase/CardinalUseCaseTest.kt rename to src/test/kotlin/com/weeth/domain/user/application/usecase/command/CardinalUseCaseTest.kt index b22de9b1..885f4261 100644 --- a/src/test/kotlin/com/weeth/domain/user/application/usecase/CardinalUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/CardinalUseCaseTest.kt @@ -1,31 +1,29 @@ -package com.weeth.domain.user.application.usecase +package com.weeth.domain.user.application.usecase.command import com.weeth.domain.user.application.dto.request.CardinalSaveRequest import com.weeth.domain.user.application.dto.request.CardinalUpdateRequest import com.weeth.domain.user.application.dto.response.CardinalResponse import com.weeth.domain.user.application.mapper.CardinalMapper +import com.weeth.domain.user.application.usecase.query.GetCardinalQueryService import com.weeth.domain.user.domain.entity.Cardinal import com.weeth.domain.user.domain.entity.enums.CardinalStatus -import com.weeth.domain.user.domain.service.CardinalGetService -import com.weeth.domain.user.domain.service.CardinalSaveService +import com.weeth.domain.user.domain.repository.CardinalRepository import com.weeth.domain.user.fixture.CardinalTestFixture import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import io.mockk.Runs import io.mockk.every -import io.mockk.just import io.mockk.mockk import io.mockk.verify import java.time.LocalDateTime +import java.util.Optional class CardinalUseCaseTest : DescribeSpec({ - - val cardinalGetService = mockk() - val cardinalSaveService = mockk() + val cardinalRepository = mockk() val cardinalMapper = mockk() - val useCase = CardinalUseCase(cardinalGetService, cardinalSaveService, cardinalMapper) + val manageCardinalUseCase = ManageCardinalUseCase(cardinalRepository, cardinalMapper) + val getCardinalQueryService = GetCardinalQueryService(cardinalRepository, cardinalMapper) describe("save") { context("진행중이 아닌 기수라면") { @@ -34,15 +32,15 @@ class CardinalUseCaseTest : val toSave = CardinalTestFixture.createCardinal(cardinalNumber = 7, year = 2025, semester = 1) val saved = CardinalTestFixture.createCardinal(cardinalNumber = 7, year = 2025, semester = 1) - every { cardinalGetService.validateCardinal(7) } just Runs - every { cardinalMapper.from(request) } returns toSave - every { cardinalSaveService.save(toSave) } returns saved + every { cardinalRepository.findByCardinalNumber(7) } returns Optional.empty() + every { cardinalMapper.toEntity(request) } returns toSave + every { cardinalRepository.save(toSave) } returns saved - useCase.save(request) + manageCardinalUseCase.save(request) - verify { cardinalGetService.validateCardinal(7) } - verify { cardinalSaveService.save(toSave) } - verify(exactly = 0) { cardinalGetService.findInProgress() } + verify { cardinalRepository.findByCardinalNumber(7) } + verify { cardinalRepository.save(toSave) } + verify(exactly = 0) { cardinalRepository.findAllByStatus(CardinalStatus.IN_PROGRESS) } } } @@ -56,15 +54,15 @@ class CardinalUseCaseTest : val newCardinalAfterSave = CardinalTestFixture.createCardinal(cardinalNumber = 7, year = 2025, semester = 1) - every { cardinalGetService.validateCardinal(7) } just Runs - every { cardinalGetService.findInProgress() } returns listOf(oldCardinal) - every { cardinalMapper.from(request) } returns newCardinalBeforeSave - every { cardinalSaveService.save(newCardinalBeforeSave) } returns newCardinalAfterSave + every { cardinalRepository.findByCardinalNumber(7) } returns Optional.empty() + every { cardinalRepository.findAllByStatus(CardinalStatus.IN_PROGRESS) } returns listOf(oldCardinal) + every { cardinalMapper.toEntity(request) } returns newCardinalBeforeSave + every { cardinalRepository.save(newCardinalBeforeSave) } returns newCardinalAfterSave - useCase.save(request) + manageCardinalUseCase.save(request) - verify { cardinalGetService.findInProgress() } - verify { cardinalSaveService.save(newCardinalBeforeSave) } + verify { cardinalRepository.findAllByStatus(CardinalStatus.IN_PROGRESS) } + verify { cardinalRepository.save(newCardinalBeforeSave) } oldCardinal.status shouldBe CardinalStatus.DONE newCardinalAfterSave.status shouldBe CardinalStatus.IN_PROGRESS @@ -75,9 +73,9 @@ class CardinalUseCaseTest : describe("update") { it("연도와 학기를 변경한다") { val cardinal = CardinalTestFixture.createCardinal(cardinalNumber = 6, year = 2024, semester = 2) - val dto = CardinalUpdateRequest(1L, 2025, 1, false) + every { cardinalRepository.findById(1L) } returns Optional.of(cardinal) - cardinal.update(dto) + manageCardinalUseCase.update(CardinalUpdateRequest(1L, 2025, 1, false)) cardinal.year shouldBe 2025 cardinal.semester shouldBe 1 @@ -97,18 +95,18 @@ class CardinalUseCaseTest : val response2 = CardinalResponse(2L, 7, 2025, 1, CardinalStatus.IN_PROGRESS, now.minusDays(2), now) - every { cardinalGetService.findAll() } returns cardinals - every { cardinalMapper.to(cardinal1) } returns response1 - every { cardinalMapper.to(cardinal2) } returns response2 + every { cardinalRepository.findAllByOrderByCardinalNumberAsc() } returns cardinals + every { cardinalMapper.toResponse(cardinal1) } returns response1 + every { cardinalMapper.toResponse(cardinal2) } returns response2 - val responses = useCase.findAll() + val responses = getCardinalQueryService.findAll() - verify { cardinalGetService.findAll() } - verify(exactly = 2) { cardinalMapper.to(any()) } + verify { cardinalRepository.findAllByOrderByCardinalNumberAsc() } + verify(exactly = 2) { cardinalMapper.toResponse(any()) } responses shouldHaveSize 2 - responses.map { it.cardinalNumber() } shouldBe listOf(6, 7) - responses.map { it.status() } shouldBe listOf(CardinalStatus.DONE, CardinalStatus.IN_PROGRESS) + responses.map { it.cardinalNumber } shouldBe listOf(6, 7) + responses.map { it.status } shouldBe listOf(CardinalStatus.DONE, CardinalStatus.IN_PROGRESS) } } }) diff --git a/src/test/kotlin/com/weeth/domain/user/application/usecase/query/GetUserQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/user/application/usecase/query/GetUserQueryServiceTest.kt new file mode 100644 index 00000000..b73993c5 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/user/application/usecase/query/GetUserQueryServiceTest.kt @@ -0,0 +1,94 @@ +package com.weeth.domain.user.application.usecase.query + +import com.weeth.domain.user.application.dto.response.UserDetailsResponse +import com.weeth.domain.user.application.dto.response.UserProfileResponse +import com.weeth.domain.user.application.mapper.UserMapper +import com.weeth.domain.user.domain.entity.UserCardinal +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalReader +import com.weeth.domain.user.domain.repository.UserCardinalRepository +import com.weeth.domain.user.domain.repository.UserReader +import com.weeth.domain.user.domain.repository.UserRepository +import com.weeth.domain.user.fixture.CardinalTestFixture +import com.weeth.domain.user.fixture.UserTestFixture +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk + +class GetUserQueryServiceTest : + DescribeSpec({ + val userRepository = mockk() + val userReader = mockk() + val cardinalReader = mockk() + val userCardinalRepository = mockk() + val userCardinalReader = mockk() + val mapper = mockk() + + val queryService = + GetUserQueryService( + userRepository, + userReader, + cardinalReader, + userCardinalRepository, + userCardinalReader, + mapper, + ) + + describe("existsByEmail") { + it("repository exists 결과를 반환한다") { + every { userRepository.existsByEmailValue("foo@bar.com") } returns true + + queryService.existsByEmail("foo@bar.com") shouldBe true + } + } + + describe("findUserDetails") { + it("user와 cardinal 목록을 조회해 UserDetailsResponse로 매핑한다") { + val user = UserTestFixture.createActiveUser1(1L) + val cardinal = CardinalTestFixture.createCardinal(id = 10L, cardinalNumber = 6, year = 2024, semester = 2) + val userCardinals = listOf(UserCardinal(user, cardinal)) + val response = + UserDetailsResponse( + 1, + user.name, + user.emailValue, + user.studentId, + user.department, + listOf(6), + user.role, + ) + + every { userReader.getById(1L) } returns user + every { userCardinalReader.findAllByUser(user) } returns userCardinals + every { mapper.toUserDetailsResponse(user, userCardinals) } returns response + + queryService.findUserDetails(1L) shouldBe response + } + } + + describe("findMyProfile") { + it("내 프로필을 UserProfileResponse로 매핑한다") { + val user = UserTestFixture.createActiveUser1(2L) + val cardinal = CardinalTestFixture.createCardinal(id = 11L, cardinalNumber = 7, year = 2025, semester = 1) + val userCardinals = listOf(UserCardinal(user, cardinal)) + val response = + UserProfileResponse( + 2, + user.name, + user.emailValue, + user.studentId, + user.telValue, + user.department, + listOf(7), + user.role, + ) + + every { userReader.getById(2L) } returns user + every { userCardinalReader.findAllByUser(user) } returns userCardinals + every { mapper.toUserProfileResponse(user, userCardinals) } returns response + + queryService.findMyProfile(2L) shouldBe response + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/user/domain/entity/CardinalTest.kt b/src/test/kotlin/com/weeth/domain/user/domain/entity/CardinalTest.kt new file mode 100644 index 00000000..14edcf3d --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/user/domain/entity/CardinalTest.kt @@ -0,0 +1,26 @@ +package com.weeth.domain.user.domain.entity + +import com.weeth.domain.user.domain.entity.enums.CardinalStatus +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class CardinalTest : + StringSpec({ + "inProgress/done 상태 전환" { + val cardinal = Cardinal(cardinalNumber = 10, year = 2026, semester = 1) + + cardinal.inProgress() + cardinal.status shouldBe CardinalStatus.IN_PROGRESS + + cardinal.done() + cardinal.status shouldBe CardinalStatus.DONE + } + + "update는 year/semester를 변경한다" { + val cardinal = Cardinal(cardinalNumber = 9, year = 2025, semester = 2) + cardinal.update(2026, 1) + + cardinal.year shouldBe 2026 + cardinal.semester shouldBe 1 + } + }) diff --git a/src/test/kotlin/com/weeth/domain/user/domain/entity/UserTest.kt b/src/test/kotlin/com/weeth/domain/user/domain/entity/UserTest.kt new file mode 100644 index 00000000..6d892f14 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/user/domain/entity/UserTest.kt @@ -0,0 +1,40 @@ +package com.weeth.domain.user.domain.entity + +import com.weeth.domain.user.domain.entity.enums.Role +import com.weeth.domain.user.domain.entity.enums.Status +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class UserTest : + StringSpec({ + "accept/ban/leave 상태 전환" { + val user = User(name = "test", email = "test@test.com", studentId = "20200001") + + user.accept() + user.status shouldBe Status.ACTIVE + + user.ban() + user.status shouldBe Status.BANNED + + user.leave() + user.status shouldBe Status.LEFT + } + + "attendance 카운터 및 출석률 계산" { + val user = User(name = "test", email = "test@test.com", studentId = "20200001") + user.attend() + user.attend() + user.absent() + + user.attendanceCount shouldBe 2 + user.absenceCount shouldBe 1 + user.attendanceRate shouldBe (2 * 100 / 3) + } + + "updateRole / hasRole" { + val user = User(name = "test", email = "test@test.com", studentId = "20200001") + user.updateRole(Role.ADMIN) + + user.hasRole(Role.ADMIN) shouldBe true + } + }) diff --git a/src/test/kotlin/com/weeth/domain/user/domain/service/CardinalGetServiceTest.kt b/src/test/kotlin/com/weeth/domain/user/domain/service/CardinalGetServiceTest.kt deleted file mode 100644 index b900c9a2..00000000 --- a/src/test/kotlin/com/weeth/domain/user/domain/service/CardinalGetServiceTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.weeth.domain.user.domain.service - -import com.weeth.domain.user.application.exception.DuplicateCardinalException -import com.weeth.domain.user.domain.entity.Cardinal -import com.weeth.domain.user.domain.repository.CardinalRepository -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import java.util.Optional - -class CardinalGetServiceTest : - DescribeSpec({ - - val cardinalRepository = mockk() - val cardinalGetService = CardinalGetService(cardinalRepository) - - describe("findByAdminSide") { - context("존재하지 않는 기수를 넣었을 때") { - it("새로 저장된다") { - every { cardinalRepository.findByCardinalNumber(7) } returns Optional.empty() - every { cardinalRepository.save(any()) } returns - Cardinal.builder().cardinalNumber(7).build() - - val result = cardinalGetService.findByAdminSide(7) - - result.cardinalNumber shouldBe 7 - } - } - } - - describe("validateCardinal") { - context("중복된 기수일 때") { - it("예외를 던진다") { - every { cardinalRepository.findByCardinalNumber(7) } returns - Optional.of(Cardinal.builder().cardinalNumber(7).build()) - - shouldThrow { - cardinalGetService.validateCardinal(7) - } - } - } - - context("중복되지 않는 기수일 때") { - it("예외를 던지지 않는다") { - every { cardinalRepository.findByCardinalNumber(7) } returns Optional.empty() - - shouldNotThrowAny { - cardinalGetService.validateCardinal(7) - } - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/user/domain/service/UserCardinalGetServiceTest.kt b/src/test/kotlin/com/weeth/domain/user/domain/service/UserCardinalGetServiceTest.kt deleted file mode 100644 index b784670d..00000000 --- a/src/test/kotlin/com/weeth/domain/user/domain/service/UserCardinalGetServiceTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.weeth.domain.user.domain.service - -import com.weeth.domain.user.application.exception.CardinalNotFoundException -import com.weeth.domain.user.domain.entity.UserCardinal -import com.weeth.domain.user.domain.repository.UserCardinalRepository -import com.weeth.domain.user.fixture.CardinalTestFixture -import com.weeth.domain.user.fixture.UserTestFixture -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.booleans.shouldBeFalse -import io.kotest.matchers.booleans.shouldBeTrue -import io.mockk.every -import io.mockk.mockk - -class UserCardinalGetServiceTest : - DescribeSpec({ - - val userCardinalRepository = mockk() - val userCardinalGetService = UserCardinalGetService(userCardinalRepository) - - describe("notContains") { - it("유저의 기수 목록 중 특정 기수가 없으면 true를 반환한다") { - val user = UserTestFixture.createActiveUser1() - val existingCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 7, year = 2025, semester = 2) - val targetCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 8, year = 2026, semester = 1) - val userCardinal = UserCardinal(user, existingCardinal) - - every { - userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user) - } returns listOf(userCardinal) - - val result = userCardinalGetService.notContains(user, targetCardinal) - - result.shouldBeTrue() - } - } - - describe("isCurrent") { - context("현재 유저의 최신 기수보다 최신 기수일 때") { - it("true를 반환한다") { - val user = UserTestFixture.createActiveUser1() - val oldCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 7, year = 2025, semester = 2) - val newCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 8, year = 2026, semester = 1) - val userCardinal = UserCardinal(user, oldCardinal) - - every { - userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user) - } returns listOf(userCardinal) - - val result = userCardinalGetService.isCurrent(user, newCardinal) - - result.shouldBeTrue() - } - } - - context("새 기수가 기존 최대보다 작을 때") { - it("false를 반환한다") { - val user = UserTestFixture.createActiveUser1() - val oldCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 7, year = 2025, semester = 1) - val newCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 6, year = 2024, semester = 2) - val userCardinal = UserCardinal(user, oldCardinal) - - every { - userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user) - } returns listOf(userCardinal) - - val result = userCardinalGetService.isCurrent(user, newCardinal) - - result.shouldBeFalse() - } - } - - context("유저가 어떤 기수도 가지고 있지 않을 때") { - it("CardinalNotFoundException이 발생한다") { - val user = UserTestFixture.createActiveUser1() - val newCardinal = CardinalTestFixture.createCardinal(cardinalNumber = 8, year = 2026, semester = 1) - - every { - userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user) - } returns listOf() - - shouldThrow { - userCardinalGetService.isCurrent(user, newCardinal) - } - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/user/domain/service/UserCardinalPolicyTest.kt b/src/test/kotlin/com/weeth/domain/user/domain/service/UserCardinalPolicyTest.kt new file mode 100644 index 00000000..3b606765 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/user/domain/service/UserCardinalPolicyTest.kt @@ -0,0 +1,63 @@ +package com.weeth.domain.user.domain.service + +import com.weeth.domain.user.application.exception.CardinalNotFoundException +import com.weeth.domain.user.domain.repository.UserCardinalReader +import com.weeth.domain.user.fixture.CardinalTestFixture +import com.weeth.domain.user.fixture.UserCardinalTestFixture +import com.weeth.domain.user.fixture.UserTestFixture +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk + +class UserCardinalPolicyTest : + DescribeSpec({ + val userCardinalReader = mockk() + val policy = UserCardinalPolicy(userCardinalReader) + + describe("getCurrentCardinal") { + it("가장 큰 기수 번호를 반환한다") { + val user = UserTestFixture.createActiveUser1(1L) + val cardinal5 = CardinalTestFixture.createCardinal(id = 2L, cardinalNumber = 5, year = 2025, semester = 1) + + every { userCardinalReader.findTopByUserOrderByCardinalNumberDesc(user) } returns + UserCardinalTestFixture.linkUserCardinal(user, cardinal5) + + policy.getCurrentCardinal(user).cardinalNumber shouldBe 5 + } + + it("기수 이력이 없으면 예외를 던진다") { + val user = UserTestFixture.createActiveUser1(1L) + every { userCardinalReader.findTopByUserOrderByCardinalNumberDesc(user) } returns null + + shouldThrow { + policy.getCurrentCardinal(user) + } + } + } + + describe("notContains") { + it("이미 포함된 기수면 false를 반환한다") { + val user = UserTestFixture.createActiveUser1(1L) + val cardinal = CardinalTestFixture.createCardinal(id = 2L, cardinalNumber = 5, year = 2025, semester = 1) + every { userCardinalReader.findAllByUser(user) } returns listOf(UserCardinalTestFixture.linkUserCardinal(user, cardinal)) + + policy.notContains(user, cardinal).shouldBeFalse() + } + } + + describe("isCurrent") { + it("신규 기수가 현재 기수보다 크면 true를 반환한다") { + val user = UserTestFixture.createActiveUser1(1L) + val current = CardinalTestFixture.createCardinal(id = 1L, cardinalNumber = 4, year = 2024, semester = 2) + val next = CardinalTestFixture.createCardinal(id = 2L, cardinalNumber = 5, year = 2025, semester = 1) + every { userCardinalReader.findTopByUserOrderByCardinalNumberDesc(user) } returns + UserCardinalTestFixture.linkUserCardinal(user, current) + + policy.isCurrent(user, next).shouldBeTrue() + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/user/domain/service/UserGetServiceTest.kt b/src/test/kotlin/com/weeth/domain/user/domain/service/UserGetServiceTest.kt deleted file mode 100644 index 7b8cc38d..00000000 --- a/src/test/kotlin/com/weeth/domain/user/domain/service/UserGetServiceTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.weeth.domain.user.domain.service - -import com.weeth.domain.user.application.exception.UserNotFoundException -import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.repository.UserRepository -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.mockk.every -import io.mockk.mockk -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.SliceImpl -import java.util.Optional - -class UserGetServiceTest : - DescribeSpec({ - - val userRepository = mockk() - val userGetService = UserGetService(userRepository) - - describe("find(Long)") { - context("존재하지 않는 유저일 때") { - it("예외를 던진다") { - val userId = 1L - every { userRepository.findById(userId) } returns Optional.empty() - - shouldThrow { - userGetService.find(userId) - } - } - } - } - - describe("find(String)") { - context("존재하지 않는 유저일 때") { - it("예외를 던진다") { - val email = "test@test.com" - every { userRepository.findByEmail(email) } returns Optional.empty() - - shouldThrow { - userGetService.find(email) - } - } - } - } - - describe("findAll(Pageable)") { - context("빈 슬라이스 반환 시") { - it("유저 예외를 던진다") { - val pageable = PageRequest.of(0, 10) - val emptySlice = SliceImpl(listOf(), pageable, false) - - every { - userRepository.findAllByStatusOrderedByCardinalAndName(any(), eq(pageable)) - } returns emptySlice - - shouldThrow { - userGetService.findAll(pageable) - } - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/user/fixture/CardinalTestFixture.kt b/src/test/kotlin/com/weeth/domain/user/fixture/CardinalTestFixture.kt index 8097dfa8..fd715201 100644 --- a/src/test/kotlin/com/weeth/domain/user/fixture/CardinalTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/user/fixture/CardinalTestFixture.kt @@ -10,14 +10,13 @@ object CardinalTestFixture { year: Int, semester: Int, ): Cardinal = - Cardinal - .builder() - .id(id) - .cardinalNumber(cardinalNumber) - .year(year) - .semester(semester) - .status(CardinalStatus.DONE) - .build() + Cardinal( + id = id ?: 0L, + cardinalNumber = cardinalNumber, + year = year, + semester = semester, + status = CardinalStatus.DONE, + ) fun createCardinalInProgress( id: Long? = null, @@ -25,12 +24,11 @@ object CardinalTestFixture { year: Int, semester: Int, ): Cardinal = - Cardinal - .builder() - .id(id) - .cardinalNumber(cardinalNumber) - .year(year) - .semester(semester) - .status(CardinalStatus.IN_PROGRESS) - .build() + Cardinal( + id = id ?: 0L, + cardinalNumber = cardinalNumber, + year = year, + semester = semester, + status = CardinalStatus.IN_PROGRESS, + ) } diff --git a/src/test/kotlin/com/weeth/domain/user/fixture/UserTestFixture.kt b/src/test/kotlin/com/weeth/domain/user/fixture/UserTestFixture.kt index a28777c4..f77be4a2 100644 --- a/src/test/kotlin/com/weeth/domain/user/fixture/UserTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/user/fixture/UserTestFixture.kt @@ -6,48 +6,43 @@ import com.weeth.domain.user.domain.entity.enums.Status object UserTestFixture { fun createActiveUser1(id: Long? = null): User = - User - .builder() - .id(id) - .name("적순") - .email("test1@test.com") - .status(Status.ACTIVE) - .build() + User( + id = id ?: 0L, + name = "적순", + email = "test1@test.com", + status = Status.ACTIVE, + ) fun createActiveUser2(id: Long? = null): User = - User - .builder() - .id(id) - .name("적순2") - .email("test2@test.com") - .status(Status.ACTIVE) - .build() + User( + id = id ?: 0L, + name = "적순2", + email = "test2@test.com", + status = Status.ACTIVE, + ) fun createWaitingUser1(id: Long? = null): User = - User - .builder() - .id(id) - .name("순적") - .email("test2@test.com") - .status(Status.WAITING) - .build() + User( + id = id ?: 0L, + name = "순적", + email = "test2@test.com", + status = Status.WAITING, + ) fun createWaitingUser2(id: Long? = null): User = - User - .builder() - .id(id) - .name("순적2") - .email("test3@test.com") - .status(Status.WAITING) - .build() + User( + id = id ?: 0L, + name = "순적2", + email = "test3@test.com", + status = Status.WAITING, + ) fun createAdmin(id: Long? = null): User = - User - .builder() - .id(id) - .name("적순") - .email("admin@test.com") - .status(Status.ACTIVE) - .role(Role.ADMIN) - .build() + User( + id = id ?: 0L, + name = "적순", + email = "admin@test.com", + status = Status.ACTIVE, + role = Role.ADMIN, + ) }