diff --git a/backend/src/main/java/com/postdm/backend/domain/auth/api/AuthController.java b/backend/src/main/java/com/postdm/backend/domain/auth/api/AuthController.java index 118ccb2..a7c2d9b 100644 --- a/backend/src/main/java/com/postdm/backend/domain/auth/api/AuthController.java +++ b/backend/src/main/java/com/postdm/backend/domain/auth/api/AuthController.java @@ -4,7 +4,9 @@ import com.postdm.backend.domain.auth.dto.IdCheckRequestDto; import com.postdm.backend.domain.auth.dto.SignInRequestDto; import com.postdm.backend.domain.auth.dto.SignUpRequestDto; -import com.postdm.backend.domain.member.domain.entity.Member; +import com.postdm.backend.domain.email.application.EmailService; +import com.postdm.backend.domain.email.dto.CheckCertificationRequestDto; +import com.postdm.backend.global.jwt.dto.TokenInfo; import com.postdm.backend.global.template.ResponseTemplate; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -12,7 +14,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -24,37 +25,54 @@ @RequestMapping("/api/v1/auth") public class AuthController { // 로그인 및 회원 가입 컨트롤러 - @Autowired - private AuthService authService; + private final AuthService authService; - @Operation(summary = "아이디 중복 확인 컨트롤러") + private final EmailService emailService; + + public AuthController(AuthService authService, EmailService emailService) { + this.authService = authService; + this.emailService = emailService; + } + + @Operation(summary = "아이디 중복 확인 컨트롤러", description = "아이디 중복 확인을 요청하는 컨트롤러 입니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), }) @PostMapping("/id-check") // 아이디 중복 확인 요청 - public ResponseTemplate idCheck(@RequestBody IdCheckRequestDto idCheckRequestDto) { - String username = authService.idCheck(idCheckRequestDto.getUsername()); - return new ResponseTemplate<>(HttpStatus.OK, "사용할 수 있는 아이디 입니다.", username); + public ResponseTemplate idCheck(@RequestBody @Valid IdCheckRequestDto idCheckRequestDto) { + authService.idCheck(idCheckRequestDto.getUsername()); + return new ResponseTemplate<>(HttpStatus.OK, "사용할 수 있는 아이디 입니다."); + } + + @Operation(summary = "이메일 인증 확인 컨트롤러", description = "이메일 인증 번호 확인을 요청하는 컨트롤러 입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + @PostMapping("/check-certification") + public ResponseTemplate checkCertificationNumber(@RequestBody @Valid CheckCertificationRequestDto checkCertificationRequestDto) { + emailService.checkCertificationNumber(checkCertificationRequestDto); + + return new ResponseTemplate<>(HttpStatus.OK, "이메일 인증 성공"); } - @Operation(summary = "회원가입 컨트롤러") + @Operation(summary = "회원가입 컨트롤러", description = "회원가입을 요청하는 컨트롤러 입니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), }) @PostMapping("/sign-up") // 회원 가입 요청 - public ResponseTemplate signUp(@RequestBody @Valid SignUpRequestDto signUpRequestDto) { - Member member = authService.signUp(signUpRequestDto); + public ResponseTemplate signUp(@RequestBody @Valid SignUpRequestDto signUpRequestDto) { + authService.signUp(signUpRequestDto); - return new ResponseTemplate<>(HttpStatus.OK, "회원가입 성공", member); + return new ResponseTemplate<>(HttpStatus.OK, "회원가입 성공"); } - @Operation(summary = "로그인 컨트롤러") + @Operation(summary = "로그인 컨트롤러", description = "로그인을 요청하는 컨트롤러 입니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), }) @PostMapping("/sign-in") // 로그인 요청 - public ResponseTemplate signIn(@RequestBody @Valid SignInRequestDto signInRequestDto, HttpServletResponse response) { - String token = authService.signIn(signInRequestDto, response); + public ResponseTemplate signIn(@RequestBody @Valid SignInRequestDto signInRequestDto, HttpServletResponse response) { + TokenInfo token = authService.signIn(signInRequestDto, response); return new ResponseTemplate<>(HttpStatus.OK, "로그인 성공", token); } diff --git a/backend/src/main/java/com/postdm/backend/domain/auth/application/AuthService.java b/backend/src/main/java/com/postdm/backend/domain/auth/application/AuthService.java index 2b7fb0e..632d489 100644 --- a/backend/src/main/java/com/postdm/backend/domain/auth/application/AuthService.java +++ b/backend/src/main/java/com/postdm/backend/domain/auth/application/AuthService.java @@ -7,11 +7,12 @@ import com.postdm.backend.domain.member.domain.entity.Member; import com.postdm.backend.domain.member.domain.entity.MemberRole; import com.postdm.backend.domain.member.domain.repository.MemberRepository; +import com.postdm.backend.global.common.exception.CustomException; +import com.postdm.backend.global.common.response.ErrorCode; import com.postdm.backend.global.jwt.dto.TokenInfo; import com.postdm.backend.global.jwt.util.JwtProvider; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -19,44 +20,49 @@ @Service public class AuthService { // 로그인 및 회원가입 서비스 - @Autowired - private MemberRepository memberRepository; - - @Autowired - private CertificationRepository certificationRepository; - - @Autowired - private BCryptPasswordEncoder bCryptPasswordEncoder; - - @Autowired - private JwtProvider jwtProvider; + private final MemberRepository memberRepository; + private final CertificationRepository certificationRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final JwtProvider jwtProvider; + private final int refreshedMS; + + // 생성자 주입 방식 + public AuthService( + MemberRepository memberRepository, + CertificationRepository certificationRepository, + BCryptPasswordEncoder bCryptPasswordEncoder, + JwtProvider jwtProvider, + @Value("${jwt.expiredMS}") int refreshedMS) { + this.memberRepository = memberRepository; + this.certificationRepository = certificationRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + this.jwtProvider = jwtProvider; + this.refreshedMS = refreshedMS; + } - @Value("${jwt.expiredMS}") - private int refreshedMS; - public String idCheck(String username) { // 아이디 중복확인 서비스 + public void idCheck(String username) { // 아이디 중복확인 서비스 boolean existedUsername = memberRepository.existsByUsername(username); // 데이터베이스에서 사용자 아이디가 존재하는지 여부 if(existedUsername) { - throw new IllegalArgumentException("이미 사용중인 아이디 입니다."); + throw new CustomException(ErrorCode.DUPLICATED_ID); } - return username; } - public Member signUp(SignUpRequestDto signUpRequestDto) { // 회원가입 서비스 + public void signUp(SignUpRequestDto signUpRequestDto) { // 회원가입 서비스 String nickname = signUpRequestDto.getNickname(); String username = signUpRequestDto.getUsername(); boolean existedUsername = memberRepository.existsByUsername(username); // 데이터베이스에서 사용자 아이디가 존재하는지 여부 if (existedUsername) { - throw new IllegalArgumentException("이미 사용중인 아이디 입니다."); + throw new CustomException(ErrorCode.DUPLICATED_ID); } String password = signUpRequestDto.getPassword(); String confirmPassword = signUpRequestDto.getConfirmPassword(); if(!password.equals(confirmPassword)) { // 입력한 비밀번호 일치 여부 - throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + throw new CustomException(ErrorCode.NOT_MATCHED_PASSWORD); } String encodedPassword = bCryptPasswordEncoder.encode(password); // 비밀번호 암호화 @@ -66,7 +72,7 @@ public Member signUp(SignUpRequestDto signUpRequestDto) { // 회원가입 서비 boolean existedEmail = memberRepository.existsByEmail(email); // 데이터베이스에서 사용자 이메일이 존재하는지 여부 if (existedEmail) { - throw new IllegalArgumentException("이미 사용중인 이메일 입니다."); + throw new CustomException(ErrorCode.DUPLICATED_EMAIL); } CertificationEntity certificationEntity = certificationRepository.findByUsername(username); // 데이터베이스에서 사용자 이름으로 된 인증 번호 조회 @@ -78,7 +84,7 @@ public Member signUp(SignUpRequestDto signUpRequestDto) { // 회원가입 서비 boolean isMatched = certificationEntity.getEmail().equals(email) && bCryptPasswordEncoder.matches(certificationNumber, encodedCertificationNumber); if(!isMatched) { - throw new IllegalArgumentException("인증번호가 일치하지 않습니다."); + throw new CustomException(ErrorCode.CERTIFICATION_FAILED); } String phone = signUpRequestDto.getPhone(); @@ -94,16 +100,14 @@ public Member signUp(SignUpRequestDto signUpRequestDto) { // 회원가입 서비 memberRepository.save(member); // 데이터베이스에 멤버 저장 certificationRepository.delete(certificationEntity); // 회원가입이 완료되면 데이터베이스에서 해당 인증번호 삭제 - - return member; } - public String signIn(SignInRequestDto signInRequestDto, HttpServletResponse response) { // 로그인 서비스 + public TokenInfo signIn(SignInRequestDto signInRequestDto, HttpServletResponse response) { // 로그인 서비스 String username = signInRequestDto.getUsername(); Member member = memberRepository.findByUsername(username); if(member == null) { - throw new IllegalArgumentException("아이디 또는 비밀번호가 잘못되었습니다."); + throw new CustomException(ErrorCode.MEMBER_NOT_FOUND); } String password = signInRequestDto.getPassword(); @@ -111,17 +115,18 @@ public String signIn(SignInRequestDto signInRequestDto, HttpServletResponse resp boolean isMatched = bCryptPasswordEncoder.matches(password, encodedPassword); if(!isMatched) { - throw new IllegalArgumentException("아이디 또는 비밀번호가 잘못되었습니다."); + throw new CustomException(ErrorCode.SIGN_IN_FAILED); } String role = member.getRole().name(); TokenInfo token = jwtProvider.generateToken(username, role); // 로그인이 완료되면 토큰 생성 - String refreshToken = token.getRefreshToken(); + String refreshToken = jwtProvider.generateRefreshToken(username, role); response.addCookie(createCookie("Refresh", refreshToken)); // 쿠키에 refresh 토큰 담음 - return token.getAccessToken(); // 응답 body에는 access 토큰 반환 + + return token; // 응답 body에는 access 토큰 반환 } private Cookie createCookie(String name, String value) { // 쿠키 생성 메소드 diff --git a/backend/src/main/java/com/postdm/backend/domain/auth/dto/IdCheckRequestDto.java b/backend/src/main/java/com/postdm/backend/domain/auth/dto/IdCheckRequestDto.java index 0a5f4e9..043b89c 100644 --- a/backend/src/main/java/com/postdm/backend/domain/auth/dto/IdCheckRequestDto.java +++ b/backend/src/main/java/com/postdm/backend/domain/auth/dto/IdCheckRequestDto.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -12,6 +13,8 @@ @NoArgsConstructor public class IdCheckRequestDto { // 아이디 중복 확인 데이터 전송을 위한 DTO + @Schema(description = "사용자 아이디", example = "test123") @NotBlank + @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{7,15}$") // 영문자, 숫자를 포함한 7글자 이상 15글자 이하 private String username; } diff --git a/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignInRequestDto.java b/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignInRequestDto.java index 4b64f69..d49d05b 100644 --- a/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignInRequestDto.java +++ b/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignInRequestDto.java @@ -1,14 +1,19 @@ package com.postdm.backend.domain.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.Data; @Schema(description = "로그인 DTO") @Data public class SignInRequestDto { // 로그인 데이터 전송을 위한 DTO + @Schema(description = "사용자 아이디", example = "test123") + @NotBlank private String username; + @Schema(description = "사용자 비밀번호", example = "test123") + @NotBlank private String password; } diff --git a/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignUpRequestDto.java b/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignUpRequestDto.java index e097578..e9f1772 100644 --- a/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignUpRequestDto.java +++ b/backend/src/main/java/com/postdm/backend/domain/auth/dto/SignUpRequestDto.java @@ -14,26 +14,33 @@ @NoArgsConstructor public class SignUpRequestDto { // 회원가입 데이터 전송을 위한 DTO + @Schema(description = "사용자 이름", example = "홍길동") @NotBlank private String nickname; + @Schema(description = "사용자 아이디", example = "test123") @NotBlank @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{7,15}$") // 영문자, 숫자를 포함한 7글자 이상 15글자 이하 private String username; + @Schema(description = "사용자 비밀번호", example = "test123") @NotBlank @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@#$%^&+=!]).{8,}$") // 영문자, 특수문자, 숫자를 포함한 최소 8자 이상의 문자 private String password; + @Schema(description = "사용자 비밀번호 확인", example = "test123") private String confirmPassword; + @Schema(description = "사용자 이메일", example = "test1@test.com") @NotBlank @Email private String email; + @Schema(description = "사용자 전화번호", example = "01012345678") @NotBlank private String phone; + @Schema(description = "이메일 인증 번호", example = "0000") @NotBlank private String certificationNumber; } diff --git a/backend/src/main/java/com/postdm/backend/domain/email/api/EmailController.java b/backend/src/main/java/com/postdm/backend/domain/email/api/EmailController.java index 1e0351a..dacbfc6 100644 --- a/backend/src/main/java/com/postdm/backend/domain/email/api/EmailController.java +++ b/backend/src/main/java/com/postdm/backend/domain/email/api/EmailController.java @@ -8,7 +8,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -19,18 +18,21 @@ @RequestMapping("/api/v1/email") public class EmailController { // 이메일 관련 컨트롤러 - @Autowired - private EmailService emailService; + private final EmailService emailService; + + public EmailController(EmailService emailService) { + this.emailService = emailService; + } @Operation(summary = "회원가입용 이메일 전송 컨트롤러") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), }) @PostMapping("/email-certification") // 이메일 전송 요청 api - public ResponseTemplate emailCertification(@RequestBody @Valid EmailCertificationRequestDto emailCertificationRequestDto) { - CertificationEntity certificationEntity = emailService.emailCertification(emailCertificationRequestDto); + public ResponseTemplate emailCertification(@RequestBody @Valid EmailCertificationRequestDto emailCertificationRequestDto) { + emailService.emailCertification(emailCertificationRequestDto); - return new ResponseTemplate<>(HttpStatus.OK, "이메일이 성공적으로 발송되었습니다.", certificationEntity); + return new ResponseTemplate<>(HttpStatus.OK, "이메일이 성공적으로 발송되었습니다."); } @Operation(summary = "비밀번호 재설정용 이메일 전송 컨트롤러") @@ -38,9 +40,9 @@ public ResponseTemplate emailCertification(@RequestBody @Va @ApiResponse(responseCode = "200", description = "성공"), }) @PostMapping("/reset-certification") - public ResponseTemplate resetCertification(@RequestBody @Valid EmailCertificationRequestDto emailCertificationRequestDto) { - CertificationEntity certificationEntity = emailService.resetCertification(emailCertificationRequestDto); + public ResponseTemplate resetCertification(@RequestBody @Valid EmailCertificationRequestDto emailCertificationRequestDto) { + emailService.resetCertification(emailCertificationRequestDto); - return new ResponseTemplate<>(HttpStatus.OK, "이메일이 성공적으로 발송되었습니다.", certificationEntity); + return new ResponseTemplate<>(HttpStatus.OK, "이메일이 성공적으로 발송되었습니다."); } } diff --git a/backend/src/main/java/com/postdm/backend/domain/email/application/EmailProvider.java b/backend/src/main/java/com/postdm/backend/domain/email/application/EmailProvider.java index 9d90ed9..3cffc89 100644 --- a/backend/src/main/java/com/postdm/backend/domain/email/application/EmailProvider.java +++ b/backend/src/main/java/com/postdm/backend/domain/email/application/EmailProvider.java @@ -14,7 +14,7 @@ public class EmailProvider { // 이메일 발송을 위한 Provider private final String SUBJECT = "[포스트 디엠] 인증 메일 입니다."; - public void sendCertificationMail(String email, String certificationNumber) { + public boolean sendCertificationMail(String email, String certificationNumber) { try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); @@ -26,8 +26,10 @@ public void sendCertificationMail(String email, String certificationNumber) { helper.setText(htmlContent, true); mailSender.send(message); + return true; } catch (Exception e) { e.printStackTrace(); + return false; } } diff --git a/backend/src/main/java/com/postdm/backend/domain/email/application/EmailService.java b/backend/src/main/java/com/postdm/backend/domain/email/application/EmailService.java index 402a24f..1802bd7 100644 --- a/backend/src/main/java/com/postdm/backend/domain/email/application/EmailService.java +++ b/backend/src/main/java/com/postdm/backend/domain/email/application/EmailService.java @@ -2,54 +2,95 @@ import com.postdm.backend.domain.email.domain.entity.CertificationEntity; import com.postdm.backend.domain.email.domain.repository.CertificationRepository; +import com.postdm.backend.domain.email.dto.CheckCertificationRequestDto; import com.postdm.backend.domain.email.dto.EmailCertificationRequestDto; import com.postdm.backend.domain.member.domain.repository.MemberRepository; -import org.springframework.beans.factory.annotation.Autowired; +import com.postdm.backend.global.common.exception.CustomException; +import com.postdm.backend.global.common.response.ErrorCode; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service public class EmailService { // 이메일 관련 서비스 - @Autowired - private MemberRepository memberRepository; - - @Autowired - private CertificationRepository certificationRepository; - - @Autowired - private BCryptPasswordEncoder bCryptPasswordEncoder; - - @Autowired - private EmailProvider emailProvider; + private final MemberRepository memberRepository; + private final CertificationRepository certificationRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final EmailProvider emailProvider; + + public EmailService( + MemberRepository memberRepository, + CertificationRepository certificationRepository, + BCryptPasswordEncoder bCryptPasswordEncoder, + EmailProvider emailProvider) { + this.memberRepository = memberRepository; + this.certificationRepository = certificationRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + this.emailProvider = emailProvider; + } - public CertificationEntity emailCertification(EmailCertificationRequestDto emailCertificationRequestDto) { // 인증메일 전송 서비스 + public void emailCertification(EmailCertificationRequestDto emailCertificationRequestDto) { // 인증메일 전송 서비스 String username = emailCertificationRequestDto.getUsername(); String email = emailCertificationRequestDto.getEmail(); boolean existedEmail = memberRepository.existsByEmail(email); if (existedEmail) { - throw new IllegalArgumentException("이미 사용중인 이메일 입니다."); + throw new CustomException(ErrorCode.DUPLICATED_EMAIL); } String certificationNumber = CertificationNumber.getCertificationNumber(); - emailProvider.sendCertificationMail(email, certificationNumber); + boolean isSucceed = emailProvider.sendCertificationMail(email, certificationNumber); + + if (!isSucceed) { + throw new CustomException(ErrorCode.EMAIL_SEND_FAILED); + } CertificationEntity certificationEntity = new CertificationEntity(username, email, bCryptPasswordEncoder.encode(certificationNumber)); - return certificationRepository.save(certificationEntity); + certificationRepository.save(certificationEntity); } - public CertificationEntity resetCertification(EmailCertificationRequestDto emailCertificationRequestDto) { + public void resetCertification(EmailCertificationRequestDto emailCertificationRequestDto) { String username = emailCertificationRequestDto.getUsername(); String email = emailCertificationRequestDto.getEmail(); + boolean existedUsername = memberRepository.existsByUsername(username); + + if(!existedUsername){ + throw new CustomException(ErrorCode.MEMBER_NOT_FOUND); + } + String certificationNumber = CertificationNumber.getCertificationNumber(); - emailProvider.sendCertificationMail(email, certificationNumber); + + boolean isSucceed = emailProvider.sendCertificationMail(email, certificationNumber); + if (!isSucceed) { + throw new CustomException(ErrorCode.EMAIL_SEND_FAILED); + } CertificationEntity certificationEntity = new CertificationEntity(username, email, bCryptPasswordEncoder.encode(certificationNumber)); - return certificationRepository.save(certificationEntity); + + certificationRepository.save(certificationEntity); + } + + public void checkCertificationNumber(CheckCertificationRequestDto checkCertificationRequestDto) { + String username = checkCertificationRequestDto.getUsername(); + String email = checkCertificationRequestDto.getEmail(); + String certificationNumber = checkCertificationRequestDto.getCertificationNumber(); + + CertificationEntity certificationEntity = certificationRepository.findByUsername(username); + + if(certificationEntity == null) { + throw new CustomException(ErrorCode.MEMBER_NOT_FOUND); + } + + String encodedCertificationNumber = certificationEntity.getCertificationNumber(); + + boolean isMatched = certificationEntity.getEmail().equals(email) && bCryptPasswordEncoder.matches(certificationNumber, encodedCertificationNumber); + + if(!isMatched) { + throw new CustomException(ErrorCode.CERTIFICATION_FAILED); + } } } diff --git a/backend/src/main/java/com/postdm/backend/domain/estimate/controller/EstimateController.java b/backend/src/main/java/com/postdm/backend/domain/estimate/controller/EstimateController.java index fe19aa1..3b2c124 100644 --- a/backend/src/main/java/com/postdm/backend/domain/estimate/controller/EstimateController.java +++ b/backend/src/main/java/com/postdm/backend/domain/estimate/controller/EstimateController.java @@ -4,9 +4,12 @@ import com.postdm.backend.domain.estimate.dto.EstimateResponseDto; import com.postdm.backend.domain.estimate.service.EstimateService; import com.postdm.backend.domain.member.domain.entity.Member; +import com.postdm.backend.global.template.ResponseTemplate; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -26,14 +29,18 @@ public EstimateController(EstimateService estimateService) { @Operation(summary = "견적서 생성", description = "새로운 견적서를 생성합니다. 사용자는 content만 입력하며, title은 자동 생성됩니다.") @PostMapping - public EstimateResponseDto createEstimate(@AuthenticationPrincipal Member currentUser, - @RequestBody EstimateRequestDto requestDto) { - return estimateService.createEstimate(requestDto.getContent(), currentUser); + public ResponseTemplate createEstimate(@AuthenticationPrincipal Member currentUser, + @RequestBody @Valid EstimateRequestDto requestDto) { + + EstimateResponseDto response = estimateService.createEstimate(requestDto.getContent(), currentUser); + + return new ResponseTemplate<>(HttpStatus.CREATED, "견적서 생성 성공", response); } @Operation(summary = "견적서 조회", description = "관리자는 모든 견적서를, 사용자는 본인의 견적서만 조회합니다.") @GetMapping - public List getEstimates(@AuthenticationPrincipal Member currentUser) { - return estimateService.getEstimates(currentUser); + public ResponseTemplate> getEstimates(@AuthenticationPrincipal Member currentUser) { + List response = estimateService.getEstimates(currentUser); + return new ResponseTemplate<>(HttpStatus.OK, "견적서 조회 성공", response); } } \ No newline at end of file diff --git a/backend/src/main/java/com/postdm/backend/domain/estimate/service/EstimateService.java b/backend/src/main/java/com/postdm/backend/domain/estimate/service/EstimateService.java index 041ccc5..ef1a3a6 100644 --- a/backend/src/main/java/com/postdm/backend/domain/estimate/service/EstimateService.java +++ b/backend/src/main/java/com/postdm/backend/domain/estimate/service/EstimateService.java @@ -6,6 +6,8 @@ import com.postdm.backend.domain.estimate.service.policy.AdminEstimatePolicy; import com.postdm.backend.domain.estimate.service.policy.UserEstimatePolicy; import com.postdm.backend.domain.member.domain.entity.Member; +import com.postdm.backend.global.common.exception.CustomException; +import com.postdm.backend.global.common.response.ErrorCode; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -33,7 +35,7 @@ public EstimateService(EstimateRepository estimateRepository, public EstimateResponseDto createEstimate(String content, Member member) { if (content == null || content.trim().isEmpty()) { - throw new IllegalArgumentException("견적서 내용은 비어 있을 수 없습니다."); + throw new CustomException(ErrorCode.ESTIMATE_NULL); } if (member == null) { @@ -41,7 +43,7 @@ public EstimateResponseDto createEstimate(String content, Member member) { if (authentication != null && authentication.getPrincipal() instanceof Member) { member = (Member) authentication.getPrincipal(); } else { - throw new RuntimeException("인증된 사용자가 존재하지 않습니다."); + throw new CustomException(ErrorCode.AUTHORIZED_MEMBER_NOT_FOUND); } } @@ -63,7 +65,7 @@ public List getEstimates(Member member) { if (authentication != null && authentication.getPrincipal() instanceof Member) { member = (Member) authentication.getPrincipal(); } else { - throw new RuntimeException("인증된 사용자가 존재하지 않습니다."); + throw new CustomException(ErrorCode.AUTHORIZED_MEMBER_NOT_FOUND); } } diff --git a/backend/src/main/java/com/postdm/backend/domain/member/api/MemberController.java b/backend/src/main/java/com/postdm/backend/domain/member/api/MemberController.java index c8149a2..8ee49eb 100644 --- a/backend/src/main/java/com/postdm/backend/domain/member/api/MemberController.java +++ b/backend/src/main/java/com/postdm/backend/domain/member/api/MemberController.java @@ -1,5 +1,6 @@ package com.postdm.backend.domain.member.api; +import com.postdm.backend.domain.email.application.EmailService; import com.postdm.backend.domain.email.dto.CheckCertificationRequestDto; import com.postdm.backend.domain.member.application.MemberService; import com.postdm.backend.domain.member.domain.entity.Member; @@ -22,8 +23,11 @@ public class MemberController { private final MemberService memberService; - public MemberController(MemberService memberService) { + private final EmailService emailService; + + public MemberController(MemberService memberService, EmailService emailService) { this.memberService = memberService; + this.emailService = emailService; } @Operation(summary = "아이디 찾기 컨트롤러") @@ -46,10 +50,10 @@ public ResponseTemplate findUsername(@RequestBody @Valid FindUsernameReq @ApiResponse(responseCode = "200", description = "성공"), }) @PostMapping("/check-certification") - public ResponseTemplate checkCertificationNumber(@RequestBody @Valid CheckCertificationRequestDto checkCertificationRequestDto) { - boolean success = memberService.checkCertificationNumber(checkCertificationRequestDto); + public ResponseTemplate checkCertificationNumber(@RequestBody @Valid CheckCertificationRequestDto checkCertificationRequestDto) { + emailService.checkCertificationNumber(checkCertificationRequestDto); - return new ResponseTemplate<>(HttpStatus.OK, "이메일 인증 성공", success); + return new ResponseTemplate<>(HttpStatus.OK, "이메일 인증 성공"); } @Operation(summary = "비밀번호 재설정 컨트롤러") diff --git a/backend/src/main/java/com/postdm/backend/domain/member/application/MemberService.java b/backend/src/main/java/com/postdm/backend/domain/member/application/MemberService.java index afd6519..e8cb0ac 100644 --- a/backend/src/main/java/com/postdm/backend/domain/member/application/MemberService.java +++ b/backend/src/main/java/com/postdm/backend/domain/member/application/MemberService.java @@ -35,28 +35,6 @@ public Member findUsernameByEmail(FindUsernameRequestDto findUsernameRequestDto) return member; } - public boolean checkCertificationNumber(CheckCertificationRequestDto checkCertificationRequestDto) { - String username = checkCertificationRequestDto.getUsername(); - String email = checkCertificationRequestDto.getEmail(); - String certificationNumber = checkCertificationRequestDto.getCertificationNumber(); - - CertificationEntity certificationEntity = certificationRepository.findByUsername(username); - - if(certificationEntity == null) { - throw new IllegalArgumentException(); - } - - String encodedCertificationNumber = certificationEntity.getCertificationNumber(); - - boolean isMatched = certificationEntity.getEmail().equals(email) && bCryptPasswordEncoder.matches(certificationNumber, encodedCertificationNumber); - - if(!isMatched) { - throw new IllegalArgumentException(); - } - - return true; - } - public Member resetPassword(ResetPasswordRequestDto resetPasswordRequestDto) { String username = resetPasswordRequestDto.getUsername(); String password = resetPasswordRequestDto.getPassword(); diff --git a/backend/src/main/java/com/postdm/backend/domain/member/dto/FindUsernameRequestDto.java b/backend/src/main/java/com/postdm/backend/domain/member/dto/FindUsernameRequestDto.java index 91a82c2..1ff4318 100644 --- a/backend/src/main/java/com/postdm/backend/domain/member/dto/FindUsernameRequestDto.java +++ b/backend/src/main/java/com/postdm/backend/domain/member/dto/FindUsernameRequestDto.java @@ -10,6 +10,7 @@ @Data public class FindUsernameRequestDto { + @Schema(description = "사용자 이메일", example = "test1@test.com") @NotBlank @Email private String email; diff --git a/backend/src/main/java/com/postdm/backend/domain/member/dto/ResetPasswordRequestDto.java b/backend/src/main/java/com/postdm/backend/domain/member/dto/ResetPasswordRequestDto.java index 250fcd3..231ae74 100644 --- a/backend/src/main/java/com/postdm/backend/domain/member/dto/ResetPasswordRequestDto.java +++ b/backend/src/main/java/com/postdm/backend/domain/member/dto/ResetPasswordRequestDto.java @@ -8,12 +8,15 @@ @Data public class ResetPasswordRequestDto { + @Schema(description = "사용자 아이디", example = "test123") @NotBlank private String username; + @Schema(description = "새로운 비밀번호", example = "newpassword1") @NotBlank private String password; + @Schema(description = "새로운 비밀번호 확인", example = "newpassword1") @NotBlank private String confirmPassword; diff --git a/backend/src/main/java/com/postdm/backend/global/common/exception/CustomException.java b/backend/src/main/java/com/postdm/backend/global/common/exception/CustomException.java new file mode 100644 index 0000000..fa39fed --- /dev/null +++ b/backend/src/main/java/com/postdm/backend/global/common/exception/CustomException.java @@ -0,0 +1,16 @@ +package com.postdm.backend.global.common.exception; + +import com.postdm.backend.global.common.response.ErrorCode; +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException { + + private final ErrorCode errorCode; + + public CustomException(ErrorCode code) { + super(code.getMessage()); + this.errorCode = code; + } + +} diff --git a/backend/src/main/java/com/postdm/backend/global/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/postdm/backend/global/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..e8d2602 --- /dev/null +++ b/backend/src/main/java/com/postdm/backend/global/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,49 @@ +package com.postdm.backend.global.common.exception; + +import com.postdm.backend.global.common.response.ErrorCode; +import com.postdm.backend.global.common.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ResponseEntity httpMethodExceptionHandler(HttpRequestMethodNotSupportedException e) { + log.error("HttpRequestMethodNotSupportedException: {}", e.getMessage()); + return ResponseEntity + .status(ErrorCode.METHOD_NOT_ALLOWED.getHttpStatus()) + .body(new ErrorResponse(ErrorCode.METHOD_NOT_ALLOWED)); + } + + @ExceptionHandler(CustomException.class) + protected ResponseEntity CustomExceptionHandler(CustomException e) { + log.error("CustomException: {}", e.getMessage()); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus().value()) + .body(new ErrorResponse(e.getErrorCode())); + } + + @ExceptionHandler(Exception.class) + protected ResponseEntity internalSeverExceptionHandler(Exception e) { + log.error("INTERNAL_SERVER_ERROR: {}", e.getMessage()); + return ResponseEntity + .status(ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus().value()) + .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR)); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class}) + protected ResponseEntity validationExceptionHandler(Exception e) { + log.error("Validation Fail: {}", e.getMessage()); + return ResponseEntity + .status(ErrorCode.VALIDATION_FAIL.getHttpStatus().value()) + .body(new ErrorResponse(ErrorCode.VALIDATION_FAIL)); + } + +} diff --git a/backend/src/main/java/com/postdm/backend/global/common/response/ErrorCode.java b/backend/src/main/java/com/postdm/backend/global/common/response/ErrorCode.java new file mode 100644 index 0000000..bfd3495 --- /dev/null +++ b/backend/src/main/java/com/postdm/backend/global/common/response/ErrorCode.java @@ -0,0 +1,44 @@ +package com.postdm.backend.global.common.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + //사용자 관련 + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + AUTHORIZED_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "인증된 사용자를 찾을 수 없습니다."), + + // 로그인 및 회원가입 관련 + DUPLICATED_ID(HttpStatus.CONFLICT, "이미 사용중인 아이디 입니다."), + DUPLICATED_EMAIL(HttpStatus.CONFLICT, "이미 사용 중인 이메일 입니다."), + NOT_MATCHED_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다"), + SIGN_IN_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘못되었습니다."), + + // 아이디 찾기 및 비밀번호 재설정 관련 + NOT_FOUND_EMAIL(HttpStatus.NOT_FOUND, "존재하지 않는 이메일 입니다."), + + // 이메일 관련 + EMAIL_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이메일 전송 실패"), + CERTIFICATION_FAILED(HttpStatus.BAD_REQUEST, "인증번호가 일치하지 않습니다."), + + // 견적서 관련 + ESTIMATE_NULL(HttpStatus.BAD_REQUEST, "견적서 내용은 비어있을 수 없습니다."), + + // 서버 에러 + DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러"), + + // 유효성 에러 + VALIDATION_FAIL(HttpStatus.BAD_REQUEST, "값이 비어있거나, 잘못된 입력 형식입니다."), + + + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "잘못된 HTTP 메서드를 호출했습니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러 입니다.") + ; + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/backend/src/main/java/com/postdm/backend/global/common/response/ErrorResponse.java b/backend/src/main/java/com/postdm/backend/global/common/response/ErrorResponse.java new file mode 100644 index 0000000..be397e9 --- /dev/null +++ b/backend/src/main/java/com/postdm/backend/global/common/response/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.postdm.backend.global.common.response; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +public class ErrorResponse { + + private final LocalDateTime timestamp = LocalDateTime.now(); + private final int status; + private final String message; + + public ErrorResponse(ErrorCode code) { + this.status = code.getHttpStatus().value(); + this.message = code.getMessage(); + } +} diff --git a/backend/src/main/java/com/postdm/backend/global/jwt/dto/TokenInfo.java b/backend/src/main/java/com/postdm/backend/global/jwt/dto/TokenInfo.java index 36c4bf8..d41a7f0 100644 --- a/backend/src/main/java/com/postdm/backend/global/jwt/dto/TokenInfo.java +++ b/backend/src/main/java/com/postdm/backend/global/jwt/dto/TokenInfo.java @@ -1,23 +1,29 @@ package com.postdm.backend.global.jwt.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +@Schema(description = "JWT 토큰 DTO") @Data @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TokenInfo { // 토큰 전송을 위한 DTO + + @Schema(description = "grantType") private String grantType; + + @Schema(description = "AccessToken 값") private String accessToken; - private String refreshToken; + + @Schema(description = "해당 사용자의 role") private String role; @Builder - public TokenInfo(String grantType, String accessToken, String refreshToken, String role) { + public TokenInfo(String grantType, String accessToken, String role) { this.grantType = grantType; this.accessToken = accessToken; - this.refreshToken = refreshToken; this.role = role; } } diff --git a/backend/src/main/java/com/postdm/backend/global/jwt/util/JwtProvider.java b/backend/src/main/java/com/postdm/backend/global/jwt/util/JwtProvider.java index a12f724..7a58ac3 100644 --- a/backend/src/main/java/com/postdm/backend/global/jwt/util/JwtProvider.java +++ b/backend/src/main/java/com/postdm/backend/global/jwt/util/JwtProvider.java @@ -52,12 +52,13 @@ public String generateAccessToken(String username, String role) { // AccessToken .compact(); } - public String generateRefreshToken(String username) { // RefreshToken 생성 + public String generateRefreshToken(String username, String role) { // RefreshToken 생성 Date now = new Date(); Date refreshTokenExpiredAt = new Date(now.getTime() + refreshedMs); return Jwts.builder() .subject(username) + .claim("role", role) .issuedAt(now) .expiration(refreshTokenExpiredAt) .signWith(secretKey) @@ -91,12 +92,12 @@ public boolean validateToken(String token) { // 토큰 유효성 검사 public TokenInfo generateToken(String username, String role) { // 토큰 생성 String accessToken = generateAccessToken(username, role); - String refreshToken = generateRefreshToken(username); + String refreshToken = generateRefreshToken(username, role); return TokenInfo.builder() .grantType("Bearer") .accessToken(accessToken) - .refreshToken(refreshToken) + .role(role) .build(); } diff --git a/backend/src/main/java/com/postdm/backend/global/template/ResponseTemplate.java b/backend/src/main/java/com/postdm/backend/global/template/ResponseTemplate.java index 0e15b97..1b69f2b 100644 --- a/backend/src/main/java/com/postdm/backend/global/template/ResponseTemplate.java +++ b/backend/src/main/java/com/postdm/backend/global/template/ResponseTemplate.java @@ -1,5 +1,6 @@ package com.postdm.backend.global.template; +import jakarta.annotation.Nullable; import lombok.Data; import org.springframework.http.HttpStatus; @@ -9,9 +10,14 @@ public class ResponseTemplate { // api 응답을 위한 템플릿 String message; T data; - public ResponseTemplate(HttpStatus status, String message, T data) { + public ResponseTemplate(HttpStatus status, String message, @Nullable T data) { this.status = status.value(); this.message = message; this.data = data; } + + public ResponseTemplate(HttpStatus status, String message) { + this.status = status.value(); + this.message = message; + } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5c9e4e7..8318df5 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -18,6 +18,7 @@ springdoc: enabled: true cache: disabled: true + override-with-generic-response: false management: endpoint: