diff --git a/src/main/java/clap/server/adapter/inbound/security/SecurityConfig.java b/src/main/java/clap/server/adapter/inbound/security/SecurityConfig.java index 56d75f42..4538d189 100644 --- a/src/main/java/clap/server/adapter/inbound/security/SecurityConfig.java +++ b/src/main/java/clap/server/adapter/inbound/security/SecurityConfig.java @@ -85,6 +85,7 @@ private AbstractRequestMatcherRegistry findAllDepartments() { - return findAllDepartmentsUsecase.findAllDepartments() - .stream() - .map(DepartmentResponseMapper::toFindAllDepartmentsResponse) - .toList(); + public ResponseEntity> findAllDepartments() { + return ResponseEntity.ok(findAllDepartmentsUsecase.findAllDepartments()); } } diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java index 41a57546..266a3cd2 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java @@ -9,6 +9,7 @@ import clap.server.exception.code.FileErrorcode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -21,14 +22,11 @@ @Tag(name = "05. Admin [회원 관리]") @WebAdapter +@RequiredArgsConstructor @RequestMapping("/api/managements") public class RegisterMemberCsvController { private final RegisterMemberCSVUsecase registerMemberCSVUsecase; - public RegisterMemberCsvController(RegisterMemberCSVUsecase registerMemberCSVUsecase) { - this.registerMemberCSVUsecase = registerMemberCSVUsecase; - } - @Operation(summary = "CSV 파일로 회원 등록 API") @PostMapping("/members/upload") @Secured("ROLE_ADMIN") @@ -36,8 +34,7 @@ public ResponseEntity registerMembersFromCsv( @AuthenticationPrincipal SecurityUserDetails userInfo, @RequestParam("file") MultipartFile file) throws IOException { if (!FileTypeValidator.validCSVFile(file.getInputStream())) { - throw new AdapterException(FileErrorcode.UNSUPPORTED_FILE_TYPE); - } + throw new AdapterException(FileErrorcode.UNSUPPORTED_FILE_TYPE);} int addedCount = registerMemberCSVUsecase.registerMembersFromCsv(userInfo.getUserId(), file); return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); } diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/request/UpdateMemberRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/request/UpdateMemberRequest.java index 82ee8719..1a7ddd05 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/request/UpdateMemberRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/request/UpdateMemberRequest.java @@ -12,8 +12,6 @@ public record UpdateMemberRequest( @NotBlank @Pattern(regexp = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$", message = "올바른 이메일 형식이 아닙니다.") - @Schema(description = "회원 이메일", example = "siena@gmail.com") - String email, @NotNull @Schema(description = "승인 권한 여부") Boolean isReviewer, @NotNull @Schema(description = "부서 ID") diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/response/SendVerificationCodeRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/response/SendVerificationCodeRequest.java new file mode 100644 index 00000000..c39abb58 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/response/SendVerificationCodeRequest.java @@ -0,0 +1,11 @@ +package clap.server.adapter.inbound.web.dto.member.response; + +import jakarta.validation.constraints.NotBlank; + +public record SendVerificationCodeRequest( + @NotBlank + String nickname, + @NotBlank + String email +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/response/VerifyCodeRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/response/VerifyCodeRequest.java new file mode 100644 index 00000000..3234168e --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/response/VerifyCodeRequest.java @@ -0,0 +1,12 @@ + +package clap.server.adapter.inbound.web.dto.member.response; + +import jakarta.validation.constraints.NotBlank; + +public record VerifyCodeRequest( + @NotBlank + String email, + @NotBlank + String code +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java b/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java index c7c2efd9..9d3a443a 100644 --- a/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java +++ b/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java @@ -1,35 +1,34 @@ package clap.server.adapter.inbound.web.member; -import clap.server.adapter.inbound.security.service.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.member.response.SendVerificationCodeRequest; +import clap.server.adapter.inbound.web.dto.member.response.VerifyCodeRequest; import clap.server.application.port.inbound.member.SendVerificationEmailUsecase; import clap.server.application.port.inbound.member.VerifyEmailCodeUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; 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; @Tag(name = "00. Auth [인증번호]") @WebAdapter @RequiredArgsConstructor -@RequestMapping("/api/members") +@RequestMapping("/api") public class EmailVerificationController { private final SendVerificationEmailUsecase sendVerificationEmailUsecase; private final VerifyEmailCodeUsecase verifyEmailCodeUsecase; @Operation(summary = "인증번호 전송 API") - @PostMapping("/verification/email") - public void sendVerificationEmail(@AuthenticationPrincipal SecurityUserDetails userInfo){ - sendVerificationEmailUsecase.sendVerificationCode(userInfo.getUserId()); + @PostMapping("/verifications/email") + public void sendVerificationEmail(@RequestBody SendVerificationCodeRequest request) { + sendVerificationEmailUsecase.sendVerificationCode(request); } @Operation(summary = "인증번호 검증 API") - @PostMapping("/verification") - public void sendVerificationEmail(@AuthenticationPrincipal SecurityUserDetails userInfo, - @RequestParam String code){ - verifyEmailCodeUsecase.verifyEmailCode(userInfo.getUserId(), code); + @PostMapping("/verifications") + public void sendVerificationEmail(@RequestBody VerifyCodeRequest request) { + verifyEmailCodeUsecase.verifyEmailCode(request); } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java index 604809ec..26e19c8f 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -32,7 +32,6 @@ public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPo private final MemberPersistenceMapper memberPersistenceMapper; private final TaskRepository taskRepository; private final TaskPersistenceMapper taskPersistenceMapper; - private final JPAQueryFactory jpaQueryFactory; @Override public Optional findById(final Long id) { @@ -110,5 +109,10 @@ public Page findAllMembers(Pageable pageable) { public Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest, String sortDirection) { return memberRepository.findMembersWithFilter(pageable, filterRequest,sortDirection).map(memberPersistenceMapper::toDomain); } + + @Override + public Optional findByNicknameOrEmail(String nickname, String email) { + return memberRepository.findByNicknameAndEmail(nickname, email).map(memberPersistenceMapper::toDomain); + } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java index fc8899a4..eb573737 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java @@ -11,11 +11,8 @@ @Repository public interface MemberRepository extends JpaRepository, MemberCustomRepository { - - List findByRoleAndStatus(MemberRole role, MemberStatus status); - Optional findByStatusAndMemberId(MemberStatus memberStatus, Long memberId); Optional findByNickname(String nickname); @@ -24,7 +21,8 @@ public interface MemberRepository extends JpaRepository, Me List findAll(); // 전체 회원 조회 - Optional findByMemberIdAndIsReviewerTrue(Long memberId); + + Optional findByNicknameAndEmail(String nickname, String email); } diff --git a/src/main/java/clap/server/application/port/inbound/admin/FindAllDepartmentsUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/FindAllDepartmentsUsecase.java index f425c8d6..f2314340 100644 --- a/src/main/java/clap/server/application/port/inbound/admin/FindAllDepartmentsUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/admin/FindAllDepartmentsUsecase.java @@ -1,9 +1,9 @@ package clap.server.application.port.inbound.admin; -import clap.server.domain.model.member.Department; +import clap.server.adapter.inbound.web.dto.admin.response.FindAllDepartmentsResponse; import java.util.List; public interface FindAllDepartmentsUsecase { - List findAllDepartments(); + List findAllDepartments(); } diff --git a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java index 5af84887..1d4afe12 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java +++ b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java @@ -30,19 +30,6 @@ public int getRemainingTasks(Long memberId) { return loadMemberPort.findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); } - public String getMemberNickname(Long memberId) { - Member member = findById(memberId); - if (member.getMemberInfo() == null) { - throw new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND); - } - return member.getMemberInfo().getNickname(); - } - - public String getMemberImageUrl(Long memberId) { - Member member = findById(memberId); - return member.getImageUrl() != null ? member.getImageUrl() : "default-image-url"; - } - public List findActiveManagers() { List activeManagers = loadMemberPort.findActiveManagers(); diff --git a/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java b/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java index 8a7a3610..1056b13f 100644 --- a/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java @@ -1,5 +1,7 @@ package clap.server.application.port.inbound.member; +import clap.server.adapter.inbound.web.dto.member.response.SendVerificationCodeRequest; + public interface SendVerificationEmailUsecase { - void sendVerificationCode(Long memberId); + void sendVerificationCode(SendVerificationCodeRequest request); } diff --git a/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java b/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java index d2cf04c8..290b3d98 100644 --- a/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java @@ -1,5 +1,7 @@ package clap.server.application.port.inbound.member; +import clap.server.adapter.inbound.web.dto.member.response.VerifyCodeRequest; + public interface VerifyEmailCodeUsecase { - void verifyEmailCode(Long memberId, String code); + void verifyEmailCode(VerifyCodeRequest request); } diff --git a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java index 9aae3ca7..e83a261b 100644 --- a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java +++ b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java @@ -33,5 +33,5 @@ public interface LoadMemberPort { Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest, String sortDirection); - + Optional findByNicknameOrEmail(String nickname, String email); } diff --git a/src/main/java/clap/server/application/service/admin/FindAllDepartmentsService.java b/src/main/java/clap/server/application/service/admin/FindAllDepartmentsService.java index b30faba4..9b6f8cb5 100644 --- a/src/main/java/clap/server/application/service/admin/FindAllDepartmentsService.java +++ b/src/main/java/clap/server/application/service/admin/FindAllDepartmentsService.java @@ -1,9 +1,10 @@ package clap.server.application.service.admin; +import clap.server.adapter.inbound.web.dto.admin.response.FindAllDepartmentsResponse; +import clap.server.application.mapper.DepartmentResponseMapper; import clap.server.application.port.inbound.admin.FindAllDepartmentsUsecase; import clap.server.application.port.outbound.member.LoadDepartmentPort; import clap.server.common.annotation.architecture.ApplicationService; -import clap.server.domain.model.member.Department; import lombok.RequiredArgsConstructor; import java.util.List; @@ -14,8 +15,10 @@ public class FindAllDepartmentsService implements FindAllDepartmentsUsecase { private final LoadDepartmentPort loadDepartmentPort; @Override - public List findAllDepartments() { - return loadDepartmentPort.findActiveDepartments(); + public List findAllDepartments() { + return loadDepartmentPort.findActiveDepartments().stream() + .map(DepartmentResponseMapper::toFindAllDepartmentsResponse) + .toList(); } } diff --git a/src/main/java/clap/server/application/service/admin/ManageMemberService.java b/src/main/java/clap/server/application/service/admin/ManageMemberService.java index fe5911ad..7c4ab4d4 100644 --- a/src/main/java/clap/server/application/service/admin/ManageMemberService.java +++ b/src/main/java/clap/server/application/service/admin/ManageMemberService.java @@ -32,7 +32,7 @@ public void updateMemberInfo(Long adminId, Long memberId, UpdateMemberRequest re //TODO: 인프라팀만 담당자가 될 수 있도록 수정해야함 member.getMemberInfo().updateMemberInfoByAdmin( - request.name(), request.email(), request.isReviewer(), + request.name(), request.isReviewer(), department, request.role(), request.departmentRole()); commandMemberPort.save(member); } diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java index 0531d0f5..60d72b4b 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -5,12 +5,16 @@ import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadDepartmentPort; +import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Department; import clap.server.domain.model.member.Member; import clap.server.domain.model.member.MemberInfo; import clap.server.exception.ApplicationException; +import clap.server.exception.AuthException; +import clap.server.exception.code.AuthErrorCode; import clap.server.exception.code.DepartmentErrorCode; +import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; @@ -24,8 +28,7 @@ class RegisterMemberService implements RegisterMemberUsecase { private final MemberService memberService; private final CommandMemberPort commandMemberPort; private final LoadDepartmentPort loadDepartmentPort; - private final PasswordEncoder passwordEncoder; - private final CsvParseService csvParser; + private final LoadMemberPort loadMemberPort; @Override @Transactional @@ -34,6 +37,12 @@ public void registerMember(Long adminId, RegisterMemberRequest request) { Department department = loadDepartmentPort.findById(request.departmentId()) .orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); + loadMemberPort.findByNickname(request.nickname()).ifPresent( + member -> { + throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME); + } + ); + // TODO: 인프라팀만 담당자가 될 수 있도록 수정해야함 MemberInfo memberInfo = MemberInfo.toMemberInfo(request.name(), request.email(), request.nickname(), request.isReviewer(), department, request.role(), request.departmentRole()); diff --git a/src/main/java/clap/server/application/service/auth/EmailVerificationService.java b/src/main/java/clap/server/application/service/auth/EmailVerificationService.java index 8c062a91..d0212053 100644 --- a/src/main/java/clap/server/application/service/auth/EmailVerificationService.java +++ b/src/main/java/clap/server/application/service/auth/EmailVerificationService.java @@ -1,32 +1,38 @@ package clap.server.application.service.auth; +import clap.server.adapter.inbound.web.dto.member.response.SendVerificationCodeRequest; +import clap.server.adapter.inbound.web.dto.member.response.VerifyCodeRequest; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.member.SendVerificationEmailUsecase; import clap.server.application.port.inbound.member.VerifyEmailCodeUsecase; import clap.server.application.port.outbound.auth.otp.CommandOtpPort; import clap.server.application.port.outbound.auth.otp.LoadOtpPort; import clap.server.application.port.outbound.email.SendEmailPort; +import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.utils.VerificationCodeGenerator; import clap.server.domain.model.auth.Otp; import clap.server.domain.model.member.Member; import clap.server.exception.ApplicationException; import clap.server.exception.code.AuthErrorCode; +import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; @ApplicationService @RequiredArgsConstructor -public class EmailVerificationService implements SendVerificationEmailUsecase, VerifyEmailCodeUsecase { + public class EmailVerificationService implements SendVerificationEmailUsecase, VerifyEmailCodeUsecase { private final MemberService memberService; + private final LoadMemberPort loadMemberPort; private final SendEmailPort sendEmailPort; private final CommandOtpPort commandOtpPort; private final LoadOtpPort loadOtpPort; @Override @Transactional - public void sendVerificationCode(Long memberId) { - Member member = memberService.findActiveMember(memberId); + public void sendVerificationCode(SendVerificationCodeRequest request) { + Member member = loadMemberPort.findByNicknameOrEmail(request.nickname(), request.email()) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); String verificationCode = VerificationCodeGenerator.generateRandomCode(); commandOtpPort.save(new Otp(member.getMemberInfo().getEmail(), verificationCode)); sendEmailPort.sendVerificationEmail(member.getMemberInfo().getEmail(), member.getNickname(), verificationCode); @@ -35,17 +41,16 @@ public void sendVerificationCode(Long memberId) { @Override @Transactional - public void verifyEmailCode(Long memberId, String code) { - Member member = memberService.findActiveMember(memberId); - Otp otp = loadOtpPort.findByEmail(member.getMemberInfo().getEmail()).orElseThrow( + public void verifyEmailCode(VerifyCodeRequest request) { + Otp otp = loadOtpPort.findByEmail(request.email()).orElseThrow( () -> new ApplicationException(AuthErrorCode.VERIFICATION_CODE_EXPIRED) ); - if(!code.equals(otp.code())){ + if(!request.code().equals(otp.code())){ throw new ApplicationException(AuthErrorCode.VERIFICATION_CODE_MISMATCH); } else { - commandOtpPort.deleteByEmail(member.getMemberInfo().getEmail()); + commandOtpPort.deleteByEmail(request.email()); } } } diff --git a/src/main/java/clap/server/application/service/member/VerifyPasswordService.java b/src/main/java/clap/server/application/service/member/VerifyPasswordService.java index 0833f323..0d488f56 100644 --- a/src/main/java/clap/server/application/service/member/VerifyPasswordService.java +++ b/src/main/java/clap/server/application/service/member/VerifyPasswordService.java @@ -24,7 +24,7 @@ class VerifyPasswordService implements VerifyPasswordUseCase { @Override public void verifyPassword(Long memberId, String inputPassword) { Member member = memberService.findActiveMember(memberId); - if (!passwordEncoder.matches(member.getPassword(), inputPassword)) { + if (!passwordEncoder.matches(inputPassword, member.getPassword())) { throw new ApplicationException(MemberErrorCode.PASSWORD_VERIFY_FAILED); } } diff --git a/src/main/java/clap/server/domain/model/member/MemberInfo.java b/src/main/java/clap/server/domain/model/member/MemberInfo.java index e6a9aece..b6fca8f5 100644 --- a/src/main/java/clap/server/domain/model/member/MemberInfo.java +++ b/src/main/java/clap/server/domain/model/member/MemberInfo.java @@ -47,11 +47,10 @@ public static MemberInfo toMemberInfo(String name, String email, String nickname .build(); } - public void updateMemberInfoByAdmin(String name, String email, boolean isReviewer, + public void updateMemberInfoByAdmin(String name, boolean isReviewer, Department department, MemberRole role, String departmentRole) { assertReviewerIsManager(isReviewer, role); this.name = name; - this.email = email; this.isReviewer = isReviewer; this.department = department; this.role = role; diff --git a/src/main/java/clap/server/exception/code/MemberErrorCode.java b/src/main/java/clap/server/exception/code/MemberErrorCode.java index 85ff96a0..865fc7d5 100644 --- a/src/main/java/clap/server/exception/code/MemberErrorCode.java +++ b/src/main/java/clap/server/exception/code/MemberErrorCode.java @@ -16,7 +16,7 @@ public enum MemberErrorCode implements BaseErrorCode { CSV_PARSING_ERROR(HttpStatus.BAD_REQUEST, "MEMBER_008", "CSV 데이터 파싱 중 오류가 발생했습니다."), MEMBER_REGISTRATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER_009", "담당자만 리뷰 권한이 있습니다."), NAME_CANNOT_BE_EMPTY(HttpStatus.BAD_REQUEST, "MEMBER_010", "이름은 공백일 수 없습니다."), - + DUPLICATE_NICKNAME(HttpStatus.BAD_REQUEST,"MEMBER_011", "중복된 닉네임입니다") ; private final HttpStatus httpStatus;