diff --git a/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilLoginIdValidateRequest.java b/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilLoginIdValidateRequest.java new file mode 100644 index 00000000..3e24f744 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilLoginIdValidateRequest.java @@ -0,0 +1,11 @@ +package com.campus.campus.domain.council.application.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record StudentCouncilLoginIdValidateRequest( + @Schema(description = "회원가입 id", example = "dede1234") + @NotBlank + String loginId +) { +} diff --git a/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilSignUpRequest.java b/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilSignUpRequest.java index 34d15757..4bea711e 100644 --- a/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilSignUpRequest.java +++ b/src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilSignUpRequest.java @@ -35,6 +35,10 @@ public record StudentCouncilSignUpRequest( Long collegeId, @Schema(description = "학과 id", example = "1") - Long majorId + Long majorId, + + @Schema(description = "당선 사진 url", example = "https://www.election.com.png") + @NotBlank + String electionImageUrl ) { } diff --git a/src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilLoginResponse.java b/src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilLoginResponse.java index 1260c4b0..8e2177a9 100644 --- a/src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilLoginResponse.java +++ b/src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilLoginResponse.java @@ -32,6 +32,9 @@ public record StudentCouncilLoginResponse( String collegeName, @Schema(description = "학과 이름 (없으면 null)", example = "컴퓨터공학과") - String majorName + String majorName, + + @Schema(description = "학생회 이름", example = "가천대학교 총학생회") + String councilName ) { } diff --git a/src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilLoginMapper.java b/src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilLoginMapper.java index 4f38c97e..d896c8cd 100644 --- a/src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilLoginMapper.java +++ b/src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilLoginMapper.java @@ -30,6 +30,8 @@ public StudentCouncil createStudentCouncil(StudentCouncilSignUpRequest studentCo .school(school) .college(college) .major(major) + .electionImageUrl(studentCouncilSignUpRequest.electionImageUrl()) + .managerApproved(false) .build(); } @@ -44,7 +46,8 @@ public StudentCouncilLoginResponse toStudentCouncilLoginResponse(StudentCouncil studentCouncil.getCouncilType(), studentCouncil.getSchool().getSchoolName(), studentCouncil.getCollege() != null ? studentCouncil.getCollege().getCollegeName() : null, - studentCouncil.getMajor() != null ? studentCouncil.getMajor().getMajorName() : null + studentCouncil.getMajor() != null ? studentCouncil.getMajor().getMajorName() : null, + studentCouncil.getCouncilName() ); } diff --git a/src/main/java/com/campus/campus/domain/council/application/service/CouncilLoginService.java b/src/main/java/com/campus/campus/domain/council/application/service/CouncilLoginService.java index c4067641..20dc417b 100644 --- a/src/main/java/com/campus/campus/domain/council/application/service/CouncilLoginService.java +++ b/src/main/java/com/campus/campus/domain/council/application/service/CouncilLoginService.java @@ -8,6 +8,7 @@ import org.springframework.transaction.annotation.Transactional; import com.campus.campus.domain.council.application.dto.request.StudentCouncilFindPasswordRequest; +import com.campus.campus.domain.council.application.dto.request.StudentCouncilLoginIdValidateRequest; import com.campus.campus.domain.council.application.dto.request.StudentCouncilLoginRequest; import com.campus.campus.domain.council.application.dto.request.StudentCouncilSignUpRequest; import com.campus.campus.domain.council.application.dto.request.StudentCouncilWithdrawRequest; @@ -23,6 +24,7 @@ import com.campus.campus.domain.council.application.exception.SignupEmailNotFoundException; import com.campus.campus.domain.council.application.exception.StudentCouncilNotFoundException; import com.campus.campus.domain.council.application.mapper.StudentCouncilLoginMapper; +import com.campus.campus.domain.council.application.util.CouncilNameGenerator; import com.campus.campus.domain.council.domain.entity.StudentCouncil; import com.campus.campus.domain.council.domain.repository.StudentCouncilRepository; import com.campus.campus.domain.mail.application.exception.EmailVerificationNotFoundException; @@ -60,13 +62,14 @@ public class CouncilLoginService { private final SecurityConfig securityConfig; private final PasswordEncoder passwordEncoder; private final RedisTokenService redisTokenService; + private final CouncilNameGenerator councilNameGenerator; @Value("${jwt.refresh.expiration-seconds}") private long refreshTokenExpirationSeconds; @Transactional - public StudentCouncilLoginResponse signUp(StudentCouncilSignUpRequest studentCouncilSignUpRequest) { - if(studentCouncilRepository.existsByEmailAndDeletedAtIsNotNull(studentCouncilSignUpRequest.email())){ + public void signUp(StudentCouncilSignUpRequest studentCouncilSignUpRequest) { + if (studentCouncilRepository.existsByEmailAndDeletedAtIsNotNull(studentCouncilSignUpRequest.email())) { throw new CouncilSignupForbiddenException(); } if (studentCouncilRepository.existsByEmail(studentCouncilSignUpRequest.email())) { @@ -88,22 +91,23 @@ public StudentCouncilLoginResponse signUp(StudentCouncilSignUpRequest studentCou StudentCouncil studentCouncil = studentCouncilLoginMapper.createStudentCouncil( studentCouncilSignUpRequest, school, scope.college, scope.major); - studentCouncilRepository.save(studentCouncil); - - String accessToken = jwtProvider.createCouncilAccessToken(studentCouncil.getId()); - String refreshToken = jwtProvider.createCouncilRefreshToken(studentCouncil.getId()); + String councilName = councilNameGenerator.buildCouncilName(studentCouncil); + studentCouncil.generateCouncilName(councilName); - redisTokenService.setRefreshToken("COUNCIL", String.valueOf(studentCouncil.getId()), refreshToken, - refreshTokenExpirationSeconds); + studentCouncilRepository.save(studentCouncil); emailVerification.use(); + } - return studentCouncilLoginMapper.toStudentCouncilLoginResponse(studentCouncil, accessToken, refreshToken); + public void validateLoginId(StudentCouncilLoginIdValidateRequest studentCouncilLoginIdValidateRequest) { + if (studentCouncilRepository.existsByLoginId(studentCouncilLoginIdValidateRequest.loginId())) { + throw new LoginIdAlreadyExistsException(); + } } public StudentCouncilLoginResponse login(StudentCouncilLoginRequest studentCouncilLoginRequest) { StudentCouncil studentCouncil = studentCouncilRepository - .findByLoginIdAndDeletedAtIsNull(studentCouncilLoginRequest.loginId()) + .findByLoginIdAndManagerApprovedIsTrueAndDeletedAtIsNull(studentCouncilLoginRequest.loginId()) .orElseThrow(StudentCouncilNotFoundException::new); if (!passwordEncoder.matches(studentCouncilLoginRequest.password(), studentCouncil.getPassword())) { @@ -127,7 +131,8 @@ public StudentCouncilFindIdResponse findId(String email) { throw new SignupEmailNotFoundException(); } - StudentCouncil studentCouncil = studentCouncilRepository.findByEmailAndDeletedAtIsNull(email) + StudentCouncil studentCouncil = studentCouncilRepository + .findByEmailAndManagerApprovedIsTrueAndDeletedAtIsNull(email) .orElseThrow(StudentCouncilNotFoundException::new); emailVerification.use(); @@ -138,7 +143,7 @@ public StudentCouncilFindIdResponse findId(String email) { @Transactional public void findPassword(StudentCouncilFindPasswordRequest studentCouncilFindPasswordRequest) { StudentCouncil studentCouncil = studentCouncilRepository - .findByLoginIdAndDeletedAtIsNull(studentCouncilFindPasswordRequest.loginId()) + .findByLoginIdAndManagerApprovedIsTrueAndDeletedAtIsNull(studentCouncilFindPasswordRequest.loginId()) .orElseThrow(StudentCouncilNotFoundException::new); if (!studentCouncilFindPasswordRequest.email().equals(studentCouncil.getEmail())) { @@ -157,7 +162,8 @@ public void findPassword(StudentCouncilFindPasswordRequest studentCouncilFindPas @Transactional public void withdrawCouncil(Long councilId, StudentCouncilWithdrawRequest studentCouncilWithdrawRequest) { - StudentCouncil studentCouncil = studentCouncilRepository.findByIdAndDeletedAtIsNull(councilId) + StudentCouncil studentCouncil = studentCouncilRepository + .findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); if (!studentCouncilWithdrawRequest.precaution()) { diff --git a/src/main/java/com/campus/campus/domain/council/application/service/CouncilService.java b/src/main/java/com/campus/campus/domain/council/application/service/CouncilService.java index 6bdef7f9..5ef1d2d8 100644 --- a/src/main/java/com/campus/campus/domain/council/application/service/CouncilService.java +++ b/src/main/java/com/campus/campus/domain/council/application/service/CouncilService.java @@ -31,7 +31,8 @@ public class CouncilService { @Transactional public void changeEmail(Long councilId, StudentCouncilChangeEmailRequest studentCouncilChangeEmailRequest) { - StudentCouncil studentCouncil = studentCouncilRepository.findByIdAndDeletedAtIsNull(councilId) + StudentCouncil studentCouncil = studentCouncilRepository + .findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); //soft delete된 유저가 예상치 못하게 계정을 복구해야할 수도 있기에 이를 막기 위해 deleteAt이 존재하더라도 조회되게 한다. @@ -50,7 +51,8 @@ public void changeEmail(Long councilId, StudentCouncilChangeEmailRequest student @Transactional public void changePassword(Long councilId, StudentCouncilChangePasswordRequest studentCouncilChangePasswordRequest) { - StudentCouncil studentCouncil = studentCouncilRepository.findByIdAndDeletedAtIsNull(councilId) + StudentCouncil studentCouncil = studentCouncilRepository + .findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); if (!securityConfig.passwordEncoder() diff --git a/src/main/java/com/campus/campus/domain/council/application/util/CouncilNameGenerator.java b/src/main/java/com/campus/campus/domain/council/application/util/CouncilNameGenerator.java new file mode 100644 index 00000000..e6fae7da --- /dev/null +++ b/src/main/java/com/campus/campus/domain/council/application/util/CouncilNameGenerator.java @@ -0,0 +1,34 @@ +package com.campus.campus.domain.council.application.util; + +import org.springframework.stereotype.Component; + +import com.campus.campus.domain.council.domain.entity.CouncilType; +import com.campus.campus.domain.council.domain.entity.StudentCouncil; + +@Component +public class CouncilNameGenerator { + public String buildCouncilName(StudentCouncil studentCouncil) { + String schoolName = studentCouncil.getSchool() != null ? studentCouncil.getSchool().getSchoolName() : ""; + CouncilType councilType = studentCouncil.getCouncilType(); + + return switch (councilType) { + case SCHOOL_COUNCIL -> String.format("%s 총학생회", schoolName).trim(); + case COLLEGE_COUNCIL -> String.format("%s %s 학생회", schoolName, getCollegeName(studentCouncil)).trim(); + case MAJOR_COUNCIL -> String.format("%s %s 학생회", schoolName, getMajorName(studentCouncil)).trim(); + }; + } + + private String getCollegeName(StudentCouncil studentCouncil) { + if (studentCouncil.getCollege() == null) { + return ""; + } + return studentCouncil.getCollege().getCollegeName(); + } + + private String getMajorName(StudentCouncil studentCouncil) { + if (studentCouncil.getMajor() == null) { + return ""; + } + return studentCouncil.getMajor().getMajorName(); + } +} diff --git a/src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java b/src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java index 53deb4e3..3f04a9be 100644 --- a/src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java +++ b/src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java @@ -62,6 +62,15 @@ public class StudentCouncil extends BaseEntity { @JoinColumn(name = "major_id") private Major major; + @Column(name = "council_name") + private String councilName; + + @Column(name = "election_image_url") + private String electionImageUrl; + + @Column(name = "manager_approved", nullable = false) + private boolean managerApproved; + @Column(name = "deleted_at") private LocalDateTime deletedAt; @@ -73,18 +82,15 @@ public void changePassword(String newPassword) { this.password = newPassword; } - public String getFullCouncilName() { - StringBuilder fullName = new StringBuilder(); - if (school != null) - fullName.append(school.getSchoolName()); - if (college != null) - fullName.append(" ").append(college.getCollegeName()); - if (major != null) - fullName.append(" ").append(major.getMajorName()); - return fullName.append(" 학생회").toString().trim(); - } - public void changeEmail(String newEmail) { this.email = newEmail; } + + public void generateCouncilName(String councilName) { + this.councilName = councilName; + } + + public void managerApprove() { + this.managerApproved = true; + } } diff --git a/src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java b/src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java index 64b77ce1..6ee08fd7 100644 --- a/src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java +++ b/src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java @@ -1,5 +1,6 @@ package com.campus.campus.domain.council.domain.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,18 +10,24 @@ import com.campus.campus.domain.council.domain.entity.StudentCouncil; public interface StudentCouncilRepository extends JpaRepository { - Optional findByLoginIdAndDeletedAtIsNull(String loginId); + Optional findByLoginIdAndManagerApprovedIsTrueAndDeletedAtIsNull(String loginId); - Optional findByLoginId(String loginId); + Optional findByEmailAndManagerApprovedIsTrueAndDeletedAtIsNull(String email); - Optional findByEmailAndDeletedAtIsNull(String email); + @Query("SELECT sc FROM StudentCouncil sc " + + "LEFT JOIN FETCH sc.school " + + "LEFT JOIN FETCH sc.college " + + "LEFT JOIN FETCH sc.major " + + "WHERE sc.id = :councilId AND sc.deletedAt IS NULL AND sc.managerApproved IS TRUE") + Optional findByIdWithDetailsAndManagerApprovedIsTrueAndDeletedAtIsNull( + @Param("councilId") Long councilId); @Query("SELECT sc FROM StudentCouncil sc " + "LEFT JOIN FETCH sc.school " + "LEFT JOIN FETCH sc.college " + "LEFT JOIN FETCH sc.major " + - "WHERE sc.id = :councilId AND sc.deletedAt IS NULL") - Optional findByIdWithDetailsAndDeletedAtIsNull(@Param("councilId") Long councilId); + "WHERE sc.deletedAt IS NULL AND sc.managerApproved IS FALSE") + List findByManagerWithDetailsApprovedIsFalseAndDeletedAtIsNull(); boolean existsByLoginId(String loginId); @@ -28,9 +35,11 @@ public interface StudentCouncilRepository extends JpaRepository findByIdAndDeletedAtIsNull(Long councilId); + Optional findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long councilId); + + Optional findByIdAndManagerApprovedIsFalseAndDeletedAtIsNull(Long councilId); - boolean existsByIdAndDeletedAtIsNull(Long councilId); + boolean existsByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long councilId); boolean existsByEmailAndDeletedAtIsNotNull(String email); } diff --git a/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilLoginController.java b/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilLoginController.java index faa0150f..c9afdf83 100644 --- a/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilLoginController.java +++ b/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilLoginController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import com.campus.campus.domain.council.application.dto.request.StudentCouncilFindPasswordRequest; +import com.campus.campus.domain.council.application.dto.request.StudentCouncilLoginIdValidateRequest; import com.campus.campus.domain.council.application.dto.request.StudentCouncilLoginRequest; import com.campus.campus.domain.council.application.dto.request.StudentCouncilSignUpRequest; import com.campus.campus.domain.council.application.dto.request.StudentCouncilWithdrawRequest; @@ -31,11 +32,19 @@ public class StudentCouncilLoginController { @PostMapping("/signup") @Operation(summary = "학생회 회원가입") - public CommonResponse Signup( - @Valid @RequestBody StudentCouncilSignUpRequest studentCouncilSignUpRequest) { - StudentCouncilLoginResponse response = councilLoginService.signUp(studentCouncilSignUpRequest); + public CommonResponse Signup(@Valid @RequestBody StudentCouncilSignUpRequest studentCouncilSignUpRequest) { + councilLoginService.signUp(studentCouncilSignUpRequest); - return CommonResponse.success(StudentCouncilResponseCode.SIGNUP_SUCCESS, response); + return CommonResponse.success(StudentCouncilResponseCode.SIGNUP_REQUEST_SUCCESS); + } + + @PostMapping("/signup/validate") + @Operation(summary = "학생회 회원가입 id 중복 검증") + public CommonResponse validateLoginId( + @Valid @RequestBody StudentCouncilLoginIdValidateRequest studentCouncilLoginIdValidateRequest) { + councilLoginService.validateLoginId(studentCouncilLoginIdValidateRequest); + + return CommonResponse.success(StudentCouncilResponseCode.VALIDATE_LOGIN_ID_SUCCESS); } @PostMapping("/login") diff --git a/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.java b/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.java index 08e7df45..6b8c7926 100644 --- a/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.java +++ b/src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.java @@ -10,7 +10,8 @@ @Getter @AllArgsConstructor public enum StudentCouncilResponseCode implements ResponseCodeInterface { - SIGNUP_SUCCESS(200, HttpStatus.OK, "학생회 회원가입에 성공했습니다."), + SIGNUP_REQUEST_SUCCESS(200, HttpStatus.OK, "학생회 회원가입 요청에 성공했습니다."), + VALIDATE_LOGIN_ID_SUCCESS(200, HttpStatus.OK, "회원가입 로그인 id 중복 검증에 성공했습니다."), LOGIN_SUCCESS(200, HttpStatus.OK, "학생회 로그인에 성공했습니다."), FIND_ID_SUCCESS(200, HttpStatus.OK, "아이디 찾기에 성공했습니다."), FIND_PASSWORD_SUCCESS(200, HttpStatus.OK, "비밀번호 재설정에 성공했습니다."), diff --git a/src/main/java/com/campus/campus/domain/councilnotice/application/mapper/StudentCouncilNoticeMapper.java b/src/main/java/com/campus/campus/domain/councilnotice/application/mapper/StudentCouncilNoticeMapper.java index 99252794..edba68f3 100644 --- a/src/main/java/com/campus/campus/domain/councilnotice/application/mapper/StudentCouncilNoticeMapper.java +++ b/src/main/java/com/campus/campus/domain/councilnotice/application/mapper/StudentCouncilNoticeMapper.java @@ -36,7 +36,7 @@ public NoticeResponse toNoticeResponse(StudentCouncilNotice notice, List return NoticeResponse.builder() .id(notice.getId()) .writerId(notice.getWriter().getId()) - .writerName(notice.getWriter().getFullCouncilName()) + .writerName(notice.getWriter().getCouncilName()) .isWriter(notice.isWrittenByCouncil(councilId)) .title(notice.getTitle()) .content(notice.getContent()) diff --git a/src/main/java/com/campus/campus/domain/councilnotice/application/service/StudentCouncilNoticeService.java b/src/main/java/com/campus/campus/domain/councilnotice/application/service/StudentCouncilNoticeService.java index a1457077..d1aa26c9 100644 --- a/src/main/java/com/campus/campus/domain/councilnotice/application/service/StudentCouncilNoticeService.java +++ b/src/main/java/com/campus/campus/domain/councilnotice/application/service/StudentCouncilNoticeService.java @@ -49,7 +49,8 @@ public NoticeResponse create(Long councilId, NoticeRequest dto) { throw new NoticeImageLimitExceededException(); } - StudentCouncil writer = studentCouncilRepository.findByIdWithDetailsAndDeletedAtIsNull(councilId) + StudentCouncil writer = studentCouncilRepository. + findByIdWithDetailsAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); StudentCouncilNotice notice = @@ -105,6 +106,8 @@ public Page findAll(int page, int size, Long councilId) @Transactional public NoticeResponse update(Long councilId, Long noticeId, NoticeRequest dto) { + studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); if (dto.imageUrls() != null && dto.imageUrls().size() > MAX_IMAGE_COUNT) { throw new NoticeImageLimitExceededException(); @@ -145,6 +148,8 @@ public NoticeResponse update(Long councilId, Long noticeId, NoticeRequest dto) { @Transactional public void delete(Long councilId, Long noticeId) { + studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); StudentCouncilNotice notice = noticeRepository.findByIdWithFullInfo(noticeId) .orElseThrow(NoticeNotFoundException::new); diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java b/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java index dac2bda8..29c7ae89 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java @@ -39,7 +39,7 @@ public PostResponse toPostResponse(StudentCouncilPost post, List images, var builder = PostResponse.builder() .id(post.getId()) .writerId(writer.getId()) - .writerName(writer.getFullCouncilName()) + .writerName(writer.getCouncilName()) .isWriter(post.isWrittenByCouncil(currentUserId)) .category(post.getCategory()) .title(post.getTitle()) diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java index f4845f0d..207c06b7 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java @@ -54,7 +54,8 @@ public PostResponse create(Long councilId, PostRequest dto) { throw new PostImageLimitExceededException(); } - StudentCouncil writer = studentCouncilRepository.findByIdWithDetailsAndDeletedAtIsNull(councilId) + StudentCouncil writer = studentCouncilRepository + .findByIdWithDetailsAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); if (dto.thumbnailImageUrl() == null && dto.thumbnailIcon() == null) { @@ -131,6 +132,9 @@ public Page findUpcomingEvents(int page, int size, Long cu @Transactional public void delete(Long councilId, Long postId) { + studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); + StudentCouncilPost post = postRepository.findByIdWithFullInfo(postId) .orElseThrow(PostNotFoundException::new); @@ -164,6 +168,9 @@ public void delete(Long councilId, Long postId) { @Transactional public PostResponse update(Long councilId, Long postId, PostRequest dto) { + studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); + if (dto.imageUrls() != null && dto.imageUrls().size() > 10) { throw new PostImageLimitExceededException(); } diff --git a/src/main/java/com/campus/campus/domain/manager/application/dto/request/CouncilApproveOrDenyRequest.java b/src/main/java/com/campus/campus/domain/manager/application/dto/request/CouncilApproveOrDenyRequest.java new file mode 100644 index 00000000..e6da3051 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/dto/request/CouncilApproveOrDenyRequest.java @@ -0,0 +1,11 @@ +package com.campus.campus.domain.manager.application.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record CouncilApproveOrDenyRequest( + @NotNull + @Schema(description = "인증 결과", example = "true") + boolean certifyResult +) { +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/dto/request/ManagerLoginRequest.java b/src/main/java/com/campus/campus/domain/manager/application/dto/request/ManagerLoginRequest.java new file mode 100644 index 00000000..043fd316 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/dto/request/ManagerLoginRequest.java @@ -0,0 +1,15 @@ +package com.campus.campus.domain.manager.application.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record ManagerLoginRequest( + @NotBlank + @Schema(description = "로그인 id", example = "illtathebest1") + String loginId, + + @NotBlank + @Schema(description = "로그인 비밀번호", example = "illtathebest") + String password +) { +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilListResponse.java b/src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilListResponse.java new file mode 100644 index 00000000..18441d08 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilListResponse.java @@ -0,0 +1,17 @@ +package com.campus.campus.domain.manager.application.dto.response; + +import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CertifyRequestCouncilListResponse( + @Schema(description = "인증 요청한 학생회 id", example = "1") + Long councilId, + + @Schema(description = "인증 요청한 학생회 이름", example = "가천대학교 총학생회") + String councilName, + + @Schema(description = "학생회 생성시간(요청시간)", example = "2026-01-05T11:18:52.92955") + LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilResponse.java b/src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilResponse.java new file mode 100644 index 00000000..9ccd63d2 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilResponse.java @@ -0,0 +1,15 @@ +package com.campus.campus.domain.manager.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CertifyRequestCouncilResponse( + @Schema(description = "인증 요청한 학생회 id", example = "1") + Long councilId, + + @Schema(description = "인증 요청한 학생회 이름", example = "가천대학교 총학생회") + String councilName, + + @Schema(description = "학생회 당선 인증 사진 url", example = "https://www.example.com.png") + String electionImageUrl +) { +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/dto/response/CouncilApproveOrDenyResponse.java b/src/main/java/com/campus/campus/domain/manager/application/dto/response/CouncilApproveOrDenyResponse.java new file mode 100644 index 00000000..68bc31e4 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/dto/response/CouncilApproveOrDenyResponse.java @@ -0,0 +1,12 @@ +package com.campus.campus.domain.manager.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CouncilApproveOrDenyResponse( + @Schema(description = "인증한 학생회 id", example = "1") + Long councilId, + + @Schema(description = "인증 결과", example = "true") + boolean certifyResult +) { +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/dto/response/ManagerLoginResponse.java b/src/main/java/com/campus/campus/domain/manager/application/dto/response/ManagerLoginResponse.java new file mode 100644 index 00000000..d3605ab0 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/dto/response/ManagerLoginResponse.java @@ -0,0 +1,20 @@ +package com.campus.campus.domain.manager.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +public record ManagerLoginResponse( + @Schema(description = "access token", example = "랜덤 accessToken") + String accessToken, + + @Schema(description = "refresh token", example = "랜덤 refreshToken") + String refreshToken, + + @Schema(description = "관리자 ID", example = "1") + Long managerId, + + @Schema(description = "관리자 이름", example = "한승현") + String managerName +) { +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/manager/application/exception/ErrorCode.java new file mode 100644 index 00000000..28f418a4 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/exception/ErrorCode.java @@ -0,0 +1,19 @@ +package com.campus.campus.domain.manager.application.exception; + +import org.springframework.http.HttpStatus; + +import com.campus.campus.global.common.exception.ErrorCodeInterface; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorCode implements ErrorCodeInterface { + MANAGER_NOT_FOUND(2701, HttpStatus.NOT_FOUND, "해당 관리자는 존재하지 않습니다."), + PASSWORD_NOT_CORRECT(2702, HttpStatus.UNAUTHORIZED, "비밀번호가 틀렸습니다."); + + private final int code; + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/exception/ManagerNotFoundException.java b/src/main/java/com/campus/campus/domain/manager/application/exception/ManagerNotFoundException.java new file mode 100644 index 00000000..2e943a81 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/exception/ManagerNotFoundException.java @@ -0,0 +1,9 @@ +package com.campus.campus.domain.manager.application.exception; + +import com.campus.campus.global.common.exception.ApplicationException; + +public class ManagerNotFoundException extends ApplicationException { + public ManagerNotFoundException() { + super(ErrorCode.MANAGER_NOT_FOUND); + } +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/exception/PasswordNotCorrectException.java b/src/main/java/com/campus/campus/domain/manager/application/exception/PasswordNotCorrectException.java new file mode 100644 index 00000000..5df67ce0 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/exception/PasswordNotCorrectException.java @@ -0,0 +1,9 @@ +package com.campus.campus.domain.manager.application.exception; + +import com.campus.campus.global.common.exception.ApplicationException; + +public class PasswordNotCorrectException extends ApplicationException { + public PasswordNotCorrectException() { + super(ErrorCode.PASSWORD_NOT_CORRECT); + } +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/mapper/ManagerMapper.java b/src/main/java/com/campus/campus/domain/manager/application/mapper/ManagerMapper.java new file mode 100644 index 00000000..77b72dd3 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/mapper/ManagerMapper.java @@ -0,0 +1,48 @@ +package com.campus.campus.domain.manager.application.mapper; + +import org.springframework.stereotype.Component; + +import com.campus.campus.domain.council.domain.entity.StudentCouncil; +import com.campus.campus.domain.manager.application.dto.response.CertifyRequestCouncilResponse; +import com.campus.campus.domain.manager.application.dto.response.CouncilApproveOrDenyResponse; +import com.campus.campus.domain.manager.application.dto.response.CertifyRequestCouncilListResponse; +import com.campus.campus.domain.manager.application.dto.response.ManagerLoginResponse; +import com.campus.campus.domain.manager.domain.entity.Manager; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ManagerMapper { + public ManagerLoginResponse toManagerLoginResponse(Manager manager, String accessToken, String refreshToken) { + return new ManagerLoginResponse( + accessToken, + refreshToken, + manager.getId(), + manager.getManagerName() + ); + } + + public CouncilApproveOrDenyResponse toCouncilApproveOrDenyResponse(Long councilId, boolean certifyResult) { + return new CouncilApproveOrDenyResponse( + councilId, + certifyResult + ); + } + + public CertifyRequestCouncilListResponse toCertifyRequestCouncilListResponse(StudentCouncil studentCouncil) { + return new CertifyRequestCouncilListResponse( + studentCouncil.getId(), + studentCouncil.getCouncilName(), + studentCouncil.getCreatedAt() + ); + } + + public CertifyRequestCouncilResponse toCertifyRequestCouncilResponse(StudentCouncil studentCouncil) { + return new CertifyRequestCouncilResponse( + studentCouncil.getId(), + studentCouncil.getCouncilName(), + studentCouncil.getElectionImageUrl() + ); + } +} diff --git a/src/main/java/com/campus/campus/domain/manager/application/service/ManagerService.java b/src/main/java/com/campus/campus/domain/manager/application/service/ManagerService.java new file mode 100644 index 00000000..6a1885fa --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/application/service/ManagerService.java @@ -0,0 +1,134 @@ +package com.campus.campus.domain.manager.application.service; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.campus.campus.domain.council.application.exception.StudentCouncilNotFoundException; +import com.campus.campus.domain.council.domain.entity.StudentCouncil; +import com.campus.campus.domain.council.domain.repository.StudentCouncilRepository; +import com.campus.campus.domain.manager.application.dto.request.CouncilApproveOrDenyRequest; +import com.campus.campus.domain.manager.application.dto.request.ManagerLoginRequest; +import com.campus.campus.domain.manager.application.dto.response.CertifyRequestCouncilResponse; +import com.campus.campus.domain.manager.application.dto.response.CouncilApproveOrDenyResponse; +import com.campus.campus.domain.manager.application.dto.response.CertifyRequestCouncilListResponse; +import com.campus.campus.domain.manager.application.dto.response.ManagerLoginResponse; +import com.campus.campus.domain.manager.application.exception.ManagerNotFoundException; +import com.campus.campus.domain.manager.application.exception.PasswordNotCorrectException; +import com.campus.campus.domain.manager.application.mapper.ManagerMapper; +import com.campus.campus.domain.manager.domain.entity.Manager; +import com.campus.campus.domain.manager.domain.repository.ManagerRepository; +import com.campus.campus.global.util.jwt.JwtProvider; +import com.campus.campus.global.util.jwt.application.service.RedisTokenService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ManagerService { + private final StudentCouncilRepository studentCouncilRepository; + private final ManagerRepository managerRepository; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; + private final RedisTokenService redisTokenService; + private final ManagerMapper managerMapper; + private final JavaMailSender javaMailSender; + + @Value("${jwt.refresh.expiration-seconds}") + private long refreshTokenExpirationSeconds; + + public ManagerLoginResponse login(ManagerLoginRequest managerLoginRequest) { + Manager manager = managerRepository.findByLoginId(managerLoginRequest.loginId()) + .orElseThrow(ManagerNotFoundException::new); + + if (!passwordEncoder.matches(managerLoginRequest.password(), manager.getPassword())) { + throw new PasswordNotCorrectException(); + } + + String accessToken = jwtProvider.createManagerAccessToken(manager.getId()); + String refreshToken = jwtProvider.createManagerRefreshToken(manager.getId()); + + redisTokenService.setRefreshToken("MANAGER", String.valueOf(manager.getId()), refreshToken, + refreshTokenExpirationSeconds); + + return managerMapper.toManagerLoginResponse(manager, accessToken, refreshToken); + } + + @Transactional + public CouncilApproveOrDenyResponse approveOrDenyCouncil(Long councilId, + CouncilApproveOrDenyRequest councilApproveOrDenyRequest) { + StudentCouncil studentCouncil = studentCouncilRepository + .findByIdAndManagerApprovedIsFalseAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); + + boolean certifyResult = councilApproveOrDenyRequest.certifyResult(); + + if (certifyResult) { + studentCouncil.managerApprove(); + studentCouncilRepository.save(studentCouncil); + + sendCouncilApprovedMail(studentCouncil.getEmail()); + } else { + sendCouncilDeniedMail(studentCouncil.getEmail()); + } + + return managerMapper.toCouncilApproveOrDenyResponse(studentCouncil.getId(), certifyResult); + } + + public List getCertifyRequestCouncils() { + return studentCouncilRepository.findByManagerWithDetailsApprovedIsFalseAndDeletedAtIsNull() + .stream() + .map(managerMapper::toCertifyRequestCouncilListResponse) + .toList(); + } + + public CertifyRequestCouncilResponse getCertifyRequestCouncil(Long councilId) { + StudentCouncil studentCouncil = studentCouncilRepository + .findByIdAndManagerApprovedIsFalseAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); + + return managerMapper.toCertifyRequestCouncilResponse(studentCouncil); + } + + private void sendCouncilApprovedMail(String to) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject("[Campus] 학생회 계정 생성 요청 승인 결과 안내"); + message.setText( + """ + [Campus] 학생회 계정 생성 요청 승인 결과 안내드립니다. + + 축하합니다!! + 관리자 확인 결과 해당 학생회 계정 인증에 성공하였습니다. + 이제부터 학생회 대표자로서 Campus 서비스 이용이 가능합니다. + + Campus에 오신 것을 환영합니다~~~ + """ + ); + + javaMailSender.send(message); + } + + private void sendCouncilDeniedMail(String to) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject("[Campus] 학생회 계정 생성 요청 승인 결과 안내"); + message.setText( + """ + [Campus] 학생회 계정 생성 요청 승인 결과 안내드립니다. + + 죄송합니다!! + 관리자 확인 결과 해당 학생회 계정 인증이 불가능하다고 판단되어 승인하지 못하였습니다. + 자세한 내용 혹은 재인증과 관련한 사항은 관리자에게 문의 바랍니다. + """ + ); + + javaMailSender.send(message); + } +} diff --git a/src/main/java/com/campus/campus/domain/manager/domain/entity/Manager.java b/src/main/java/com/campus/campus/domain/manager/domain/entity/Manager.java new file mode 100644 index 00000000..6bb4d67e --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/domain/entity/Manager.java @@ -0,0 +1,35 @@ +package com.campus.campus.domain.manager.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Entity +@Table(name = "managers") +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Manager { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "manager_id") + private Long id; + + @Column(name = "login_id") + private String loginId; + + @Column(name = "password") + private String password; + + @Column(name = "manager_name") + private String managerName; +} diff --git a/src/main/java/com/campus/campus/domain/manager/domain/repository/ManagerRepository.java b/src/main/java/com/campus/campus/domain/manager/domain/repository/ManagerRepository.java new file mode 100644 index 00000000..a31faa68 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/domain/repository/ManagerRepository.java @@ -0,0 +1,11 @@ +package com.campus.campus.domain.manager.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.campus.campus.domain.manager.domain.entity.Manager; + +public interface ManagerRepository extends JpaRepository { + Optional findByLoginId(String loginId); +} diff --git a/src/main/java/com/campus/campus/domain/manager/presentation/ManagerController.java b/src/main/java/com/campus/campus/domain/manager/presentation/ManagerController.java new file mode 100644 index 00000000..e28a8557 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/presentation/ManagerController.java @@ -0,0 +1,71 @@ +package com.campus.campus.domain.manager.presentation; + +import java.util.List; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.campus.campus.domain.manager.application.dto.request.CouncilApproveOrDenyRequest; +import com.campus.campus.domain.manager.application.dto.request.ManagerLoginRequest; +import com.campus.campus.domain.manager.application.dto.response.CertifyRequestCouncilResponse; +import com.campus.campus.domain.manager.application.dto.response.CouncilApproveOrDenyResponse; +import com.campus.campus.domain.manager.application.dto.response.CertifyRequestCouncilListResponse; +import com.campus.campus.domain.manager.application.dto.response.ManagerLoginResponse; +import com.campus.campus.domain.manager.application.service.ManagerService; +import com.campus.campus.global.common.response.CommonResponse; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/managers") +public class ManagerController { + private final ManagerService managerService; + + @PostMapping("/login") + @Operation(summary = "관리자 로그인") + public CommonResponse login(@Valid @RequestBody ManagerLoginRequest managerLoginRequest) { + ManagerLoginResponse managerLoginResponse = managerService.login(managerLoginRequest); + + return CommonResponse.success(ManagerResponseCode.MANAGER_LOGIN_SUCCESS, managerLoginResponse); + } + + @PatchMapping("/approve/council/{councilId}") + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "학생회 계정 승인") + public CommonResponse approveCouncil( + @PathVariable Long councilId, + @Valid @RequestBody CouncilApproveOrDenyRequest councilApproveOrDenyRequest + ) { + CouncilApproveOrDenyResponse response = managerService.approveOrDenyCouncil(councilId, + councilApproveOrDenyRequest); + + return CommonResponse.success(ManagerResponseCode.COUNCIL_APPROVE_OR_DENY_SUCCESS, response); + } + + @GetMapping("/approve/council/{councilId}") + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "특정 학생회 계정 인증 요청 당선 사진 조회") + public CommonResponse getCertifyRequestCouncil(@PathVariable Long councilId) { + CertifyRequestCouncilResponse response = managerService.getCertifyRequestCouncil(councilId); + + return CommonResponse.success(ManagerResponseCode.CERTIFY_REQUEST_ELECTION_IMAGE_SUCCESS, response); + } + + @GetMapping("/approve/councils") + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "학생회 인증 요청 목록 조회") + public CommonResponse> getCertifyRequestCouncils() { + List responses = managerService.getCertifyRequestCouncils(); + + return CommonResponse.success(ManagerResponseCode.CERTIFY_REQUEST_LIST_SUCCESS, responses); + } +} diff --git a/src/main/java/com/campus/campus/domain/manager/presentation/ManagerResponseCode.java b/src/main/java/com/campus/campus/domain/manager/presentation/ManagerResponseCode.java new file mode 100644 index 00000000..e6530468 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/manager/presentation/ManagerResponseCode.java @@ -0,0 +1,21 @@ +package com.campus.campus.domain.manager.presentation; + +import org.springframework.http.HttpStatus; + +import com.campus.campus.global.common.response.ResponseCodeInterface; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ManagerResponseCode implements ResponseCodeInterface { + MANAGER_LOGIN_SUCCESS(200, HttpStatus.OK, "관리자 로그인에 성공했습니다."), + COUNCIL_APPROVE_OR_DENY_SUCCESS(200, HttpStatus.OK, "학생회 승인 혹은 거부 및 메일 전송에 성공했습니다."), + CERTIFY_REQUEST_LIST_SUCCESS(200, HttpStatus.OK, "학생회 인증 요청 목록 조회에 성공했습니다."), + CERTIFY_REQUEST_ELECTION_IMAGE_SUCCESS(200, HttpStatus.OK, "해당 학생회 인증 요청 당선 사진 조회에 성공했습니다."); + + private final int code; + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/com/campus/campus/global/annotation/CurrentManagerId.java b/src/main/java/com/campus/campus/global/annotation/CurrentManagerId.java new file mode 100644 index 00000000..699b4eb7 --- /dev/null +++ b/src/main/java/com/campus/campus/global/annotation/CurrentManagerId.java @@ -0,0 +1,21 @@ +package com.campus.campus.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.swagger.v3.oas.annotations.Parameter; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Parameter(hidden = true) // 해당 어노테이션을 통해 @CurrentManagerId 적용되면 스웨거에 ManagerId 입력칸이 보이지 않음. 어차피 서버 자동 적용. +public @interface CurrentManagerId { + + /** + * 관리자 ID가 필수인지 여부 + * + * @return true이면 토큰이 없거나 유효하지 않을 때 예외 발생, false이면 null 반환 + */ + boolean required() default true; +} diff --git a/src/main/java/com/campus/campus/global/annotation/CurrentManagerIdArgumentResolver.java b/src/main/java/com/campus/campus/global/annotation/CurrentManagerIdArgumentResolver.java new file mode 100644 index 00000000..4ef410f8 --- /dev/null +++ b/src/main/java/com/campus/campus/global/annotation/CurrentManagerIdArgumentResolver.java @@ -0,0 +1,65 @@ +package com.campus.campus.global.annotation; + +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import com.campus.campus.global.util.jwt.ManagerPrincipal; +import com.campus.campus.global.util.jwt.exception.UnAuthorizedException; + +@Component +public class CurrentManagerIdArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasAnnotation = parameter.hasParameterAnnotation(CurrentManagerId.class); + boolean hasSupportedType = + Long.class.equals(parameter.getParameterType()) || + long.class.equals(parameter.getParameterType()); + + return hasAnnotation && hasSupportedType; + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + CurrentManagerId anno = parameter.getParameterAnnotation(CurrentManagerId.class); + boolean required = anno == null || anno.required(); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 1) 인증 자체가 없거나, 인증 안된 경우 + if (authentication == null || !authentication.isAuthenticated()) { + if (required) { + throw new UnAuthorizedException(); + } + return null; + } + + Object principal = authentication.getPrincipal(); + + // 2) JwtAuthenticationFilter 에서 principal 을 ManagerPrincipal 로 넣어뒀음 + if (principal instanceof ManagerPrincipal managerPrincipal) { + Long id = managerPrincipal.getManagerId(); + if (id == null && required) { + throw new UnAuthorizedException(); + } + return id; + } + + // 3) principal 타입이 예상과 다름 (예: String "anonymousUser" 등) + if (required) { + throw new UnAuthorizedException(); + } + return null; + } +} diff --git a/src/main/java/com/campus/campus/global/config/PermitUrlConfig.java b/src/main/java/com/campus/campus/global/config/PermitUrlConfig.java index 172af128..59af7ced 100644 --- a/src/main/java/com/campus/campus/global/config/PermitUrlConfig.java +++ b/src/main/java/com/campus/campus/global/config/PermitUrlConfig.java @@ -12,6 +12,7 @@ public String[] getPublicUrl() { "/actuator/health", "/auth/login/kakao", "/auth/council/signup", + "/auth/council/signup/validate", "/auth/council/login", "/auth/council/find/id", "/auth/council/find/password", @@ -25,6 +26,7 @@ public String[] getPublicUrl() { "/search/colleges", "/search/majors", "/jwt/token/reissue", + "/managers/login", "/places/search" }; } diff --git a/src/main/java/com/campus/campus/global/config/WebMvcConfig.java b/src/main/java/com/campus/campus/global/config/WebMvcConfig.java index 06f59953..2f989392 100644 --- a/src/main/java/com/campus/campus/global/config/WebMvcConfig.java +++ b/src/main/java/com/campus/campus/global/config/WebMvcConfig.java @@ -7,6 +7,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.campus.campus.global.annotation.CurrentCouncilIdArgumentResolver; +import com.campus.campus.global.annotation.CurrentManagerIdArgumentResolver; import com.campus.campus.global.annotation.CurrentUserIdArgumentResolver; import lombok.RequiredArgsConstructor; @@ -17,10 +18,12 @@ public class WebMvcConfig implements WebMvcConfigurer { private final CurrentUserIdArgumentResolver currentUserIdArgumentResolver; private final CurrentCouncilIdArgumentResolver currentCouncilIdArgumentResolver; + private final CurrentManagerIdArgumentResolver currentManagerIdArgumentResolver; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(currentUserIdArgumentResolver); resolvers.add(currentCouncilIdArgumentResolver); + resolvers.add(currentManagerIdArgumentResolver); } } diff --git a/src/main/java/com/campus/campus/global/util/jwt/JwtAuthenticationFilter.java b/src/main/java/com/campus/campus/global/util/jwt/JwtAuthenticationFilter.java index 44a5838c..633b1eea 100644 --- a/src/main/java/com/campus/campus/global/util/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/campus/campus/global/util/jwt/JwtAuthenticationFilter.java @@ -13,6 +13,9 @@ import com.campus.campus.domain.council.application.exception.StudentCouncilNotFoundException; import com.campus.campus.domain.council.domain.entity.StudentCouncil; import com.campus.campus.domain.council.domain.repository.StudentCouncilRepository; +import com.campus.campus.domain.manager.application.exception.ManagerNotFoundException; +import com.campus.campus.domain.manager.domain.entity.Manager; +import com.campus.campus.domain.manager.domain.repository.ManagerRepository; import com.campus.campus.domain.user.application.exception.UserNotFoundException; import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.domain.user.domain.repository.UserRepository; @@ -37,6 +40,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; private final UserRepository userRepository; private final StudentCouncilRepository studentCouncilRepository; + private final ManagerRepository managerRepository; private final RedisTokenService redisTokenService; @Override @@ -88,9 +92,15 @@ private Authentication createAuthentication(String token, HttpServletRequest req principal = UserPrincipal.from(user); } else if ("COUNCIL".equals(role)) { Long councilId = Long.valueOf(subject); - StudentCouncil council = studentCouncilRepository.findByIdAndDeletedAtIsNull(councilId) + StudentCouncil council = studentCouncilRepository + .findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); principal = StudentCouncilPrincipal.from(council); + } else if ("MANAGER".equals(role)) { + Long managerId = Long.valueOf(subject); + Manager manager = managerRepository.findById(managerId) + .orElseThrow(ManagerNotFoundException::new); + principal = ManagerPrincipal.from(manager); } else { throw new InvalidJwtException(); } diff --git a/src/main/java/com/campus/campus/global/util/jwt/JwtProvider.java b/src/main/java/com/campus/campus/global/util/jwt/JwtProvider.java index 36cf57b4..541be649 100644 --- a/src/main/java/com/campus/campus/global/util/jwt/JwtProvider.java +++ b/src/main/java/com/campus/campus/global/util/jwt/JwtProvider.java @@ -101,6 +101,38 @@ public Long getCouncilIdFromAccessToken(String token) { return Long.valueOf(claims.getSubject()); } + public String createManagerAccessToken(Long managerId) { + Instant now = Instant.now(); + return Jwts.builder() + .subject(String.valueOf(managerId)) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(accessTokenExpirationSeconds))) + .claim("role", "MANAGER") + .signWith(accessKey) + .compact(); + } + + public String createManagerRefreshToken(Long managerId) { + Instant now = Instant.now(); + return Jwts.builder() + .subject(String.valueOf(managerId)) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(refreshTokenExpirationSeconds))) + .claim("role", "MANAGER") + .signWith(refreshKey) + .compact(); + } + + public Long getManagerIdFromAccessToken(String token) { + Claims claims = jwtAuthenticator.parseAccessToken(token).getPayload(); + String role = claims.get("role", String.class); + if (!"MANAGER".equals(role)) { + throw new InvalidJwtException(); + } + + return Long.valueOf(claims.getSubject()); + } + public Long getAccessTokenExpiration(String accessToken) { Date expiration = jwtAuthenticator.parseAccessToken(accessToken).getPayload().getExpiration(); long now = new Date().getTime(); diff --git a/src/main/java/com/campus/campus/global/util/jwt/ManagerPrincipal.java b/src/main/java/com/campus/campus/global/util/jwt/ManagerPrincipal.java new file mode 100644 index 00000000..c6318048 --- /dev/null +++ b/src/main/java/com/campus/campus/global/util/jwt/ManagerPrincipal.java @@ -0,0 +1,73 @@ +package com.campus.campus.global.util.jwt; + +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.campus.campus.domain.manager.domain.entity.Manager; + +import lombok.Getter; + +@Getter +public class ManagerPrincipal implements UserDetails { + private final Long managerId; + private final String managerName; + private final List authorities; + + private ManagerPrincipal( + Long managerId, + String managerName, + List authorities + ) { + this.managerId = managerId; + this.managerName = managerName; + this.authorities = authorities; + } + + public static ManagerPrincipal from(Manager manager) { + List authorities = List.of(new SimpleGrantedAuthority("ROLE_MANAGER")); + return new ManagerPrincipal( + manager.getId(), + manager.getManagerName(), + authorities + ); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return null; // 인증에서 비밀번호는 안 씀 + } + + @Override + public String getUsername() { + return managerName; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/campus/campus/global/util/jwt/application/service/TokenReissueService.java b/src/main/java/com/campus/campus/global/util/jwt/application/service/TokenReissueService.java index 7a1da902..27fa2301 100644 --- a/src/main/java/com/campus/campus/global/util/jwt/application/service/TokenReissueService.java +++ b/src/main/java/com/campus/campus/global/util/jwt/application/service/TokenReissueService.java @@ -74,7 +74,7 @@ private void checkUserExists(String role, Long id) { throw new UserNotFoundException(); } } else if ("COUNCIL".equals(role)) { - if (!studentCouncilRepository.existsByIdAndDeletedAtIsNull(id)) { + if (!studentCouncilRepository.existsByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(id)) { throw new StudentCouncilNotFoundException(); } }