diff --git a/src/main/java/com/danahub/zipitda/auth/controller/VerificationController.java b/src/main/java/com/danahub/zipitda/auth/controller/VerificationController.java new file mode 100644 index 0000000..ea2872e --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/controller/VerificationController.java @@ -0,0 +1,36 @@ +package com.danahub.zipitda.auth.controller; + +import com.danahub.zipitda.auth.dto.VerificationResponseDto; +import com.danahub.zipitda.auth.dto.VerificationSendCodeRequestDto; +import com.danahub.zipitda.auth.dto.VerificationVerifyCodeRequestDto; +import com.danahub.zipitda.auth.service.VerificationService; +import com.danahub.zipitda.common.dto.CommonResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/verification") +@RequiredArgsConstructor +public class VerificationController { + + private final VerificationService verificationService; + + // 인증 코드 발송 API + @PostMapping("/send-code") + public ResponseEntity> sendVerificationCode( + @RequestBody VerificationSendCodeRequestDto request) { + + VerificationResponseDto response = verificationService.sendCode(request); + return ResponseEntity.ok(CommonResponse.success(response)); + } + + // 인증 코드 검증 API + @PostMapping("/verify-code") + public ResponseEntity> verifyCode( + @RequestBody VerificationVerifyCodeRequestDto request) { + + VerificationResponseDto response = verificationService.verifyCode(request); + return ResponseEntity.ok(CommonResponse.success(response)); + } +} \ No newline at end of file diff --git a/src/main/java/com/danahub/zipitda/auth/domain/Verification.java b/src/main/java/com/danahub/zipitda/auth/domain/Verification.java new file mode 100644 index 0000000..7e6031c --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/domain/Verification.java @@ -0,0 +1,43 @@ +package com.danahub.zipitda.auth.domain; + +import com.danahub.zipitda.common.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.apache.ibatis.annotations.Case; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "VERIFICATIONS") +@Getter +@Setter +@Builder +@AllArgsConstructor +public class Verification extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; // PK + + @Column(nullable = false) + private String type; // 인증 유형 (email/sms) + + private String email; // 인증 email + + private String mobile; // 인증 폰 번호 + + @Column(nullable = false) + private String code; // 인증 코드 + + @Column(nullable = false) + private Boolean isVerified = false; // 인증 완료 여부 + + @Column(nullable = false) + private Integer retryCount = 0; // 재시도 횟수 + + @Column(nullable = false) + private LocalDateTime expiredAt; // 만료시간 + + public Verification() { + } +} \ No newline at end of file diff --git a/src/main/java/com/danahub/zipitda/auth/dto/VerificationResponseDto.java b/src/main/java/com/danahub/zipitda/auth/dto/VerificationResponseDto.java new file mode 100644 index 0000000..ab23ded --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/dto/VerificationResponseDto.java @@ -0,0 +1,17 @@ +package com.danahub.zipitda.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VerificationResponseDto { + private boolean success; // 요청 성공 여부 + private String message; // 응답 메시지 + private int retryCount; // 남은 재시도 횟수 + private String expiredAt; // 인증코드 만료시간 +} \ No newline at end of file diff --git a/src/main/java/com/danahub/zipitda/auth/dto/VerificationSendCodeRequestDto.java b/src/main/java/com/danahub/zipitda/auth/dto/VerificationSendCodeRequestDto.java new file mode 100644 index 0000000..13007e0 --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/dto/VerificationSendCodeRequestDto.java @@ -0,0 +1,9 @@ +package com.danahub.zipitda.auth.dto; + +import lombok.Data; + +@Data +public class VerificationSendCodeRequestDto { + private String type; // 인증 유형 (email/sms) + private String recipient; // 수신자 (이메일/전화번호) +} \ No newline at end of file diff --git a/src/main/java/com/danahub/zipitda/auth/dto/VerificationVerifyCodeRequestDto.java b/src/main/java/com/danahub/zipitda/auth/dto/VerificationVerifyCodeRequestDto.java new file mode 100644 index 0000000..9063d31 --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/dto/VerificationVerifyCodeRequestDto.java @@ -0,0 +1,10 @@ +package com.danahub.zipitda.auth.dto; + +import lombok.Data; + +@Data +public class VerificationVerifyCodeRequestDto { + private String type; // 인증 유형 (email/sms) + private String recipient; // 수신자 (이메일/전화번호) + private String code; // 인증 코드 +} diff --git a/src/main/java/com/danahub/zipitda/auth/repository/VerificationRepository.java b/src/main/java/com/danahub/zipitda/auth/repository/VerificationRepository.java new file mode 100644 index 0000000..d41b806 --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/repository/VerificationRepository.java @@ -0,0 +1,11 @@ +package com.danahub.zipitda.auth.repository; + +import com.danahub.zipitda.auth.domain.Verification; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface VerificationRepository extends JpaRepository { + Optional findFirstByTypeAndEmail(String type, String email); // 이메일로 검색 + Optional findFirstByTypeAndMobile(String type, String mobile); // 휴대폰 번호로 검색 +} \ No newline at end of file diff --git a/src/main/java/com/danahub/zipitda/auth/service/VerificationService.java b/src/main/java/com/danahub/zipitda/auth/service/VerificationService.java new file mode 100644 index 0000000..a414de0 --- /dev/null +++ b/src/main/java/com/danahub/zipitda/auth/service/VerificationService.java @@ -0,0 +1,107 @@ +package com.danahub.zipitda.auth.service; + +import com.danahub.zipitda.auth.domain.Verification; +import com.danahub.zipitda.auth.dto.VerificationResponseDto; +import com.danahub.zipitda.auth.dto.VerificationSendCodeRequestDto; +import com.danahub.zipitda.auth.dto.VerificationVerifyCodeRequestDto; +import com.danahub.zipitda.auth.repository.VerificationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.Random; + +@Service +@RequiredArgsConstructor +public class VerificationService { + + private final VerificationRepository verificationRepository; + + // 인증 코드 발송 + public VerificationResponseDto sendCode(VerificationSendCodeRequestDto request) { + validateRequest(request); + + // 인증 코드 생성 및 저장 + String code = generateCode(); + Verification verification = Verification.builder() + .type(request.getType()) + .email("email".equals(request.getType()) ? request.getRecipient() : null) + .mobile("sms".equals(request.getType()) ? request.getRecipient() : null) + .code(code) + .retryCount(0) // 명시적으로 설정 + .expiredAt(LocalDateTime.now().plusMinutes(5)) + .isVerified(false) + .build(); + + verificationRepository.save(verification); + + // DTO 변환 및 반환 + return VerificationResponseDto.builder() + .success(true) + .message("인증 코드가 성공적으로 발송되었습니다.") + .retryCount(verification.getRetryCount()) + .expiredAt(verification.getExpiredAt().toString()) + .build(); + } + + // 인증 코드 검증 + + public VerificationResponseDto verifyCode(VerificationVerifyCodeRequestDto request) { + Optional optionalVerification; + + if ("email".equals(request.getType())) { + optionalVerification = verificationRepository.findFirstByTypeAndEmail(request.getType(), request.getRecipient()); + } else if ("sms".equals(request.getType())) { + optionalVerification = verificationRepository.findFirstByTypeAndMobile(request.getType(), request.getRecipient()); + } else { + throw new IllegalArgumentException("잘못된 인증 타입입니다."); + } + + Verification verification = optionalVerification.orElseThrow(() -> + new IllegalArgumentException("인증 요청이 존재하지 않습니다.") + ); + + if (verification.getExpiredAt().isBefore(LocalDateTime.now())) { + throw new IllegalStateException("인증 코드가 만료되었습니다."); + } + + if (!verification.getCode().equals(request.getCode())) { + throw new IllegalArgumentException("인증 코드가 일치하지 않습니다."); + } + + verification.setIsVerified(true); + verificationRepository.save(verification); + + return VerificationResponseDto.builder() + .success(true) + .message("인증이 성공적으로 완료되었습니다.") + .retryCount(verification.getRetryCount()) + .expiredAt(verification.getExpiredAt().toString()) + .build(); + } + + // 요청 유효성 검증 + private void validateRequest(VerificationSendCodeRequestDto request) { + if ("email".equals(request.getType()) && !isValidEmail(request.getRecipient())) { + throw new IllegalArgumentException("유효하지 않은 이메일 형식입니다."); + } else if ("sms".equals(request.getType()) && !isValidPhoneNumber(request.getRecipient())) { + throw new IllegalArgumentException("유효하지 않은 전화번호 형식입니다."); + } + } + + // 인증 코드 생성 + private String generateCode() { + return String.format("%06d", new Random().nextInt(1000000)); + } + + // 이메일 유효성 검사 + private boolean isValidEmail(String email) { + return email != null && email.contains("@"); + } + + // 전화번호 유효성 검사 + private boolean isValidPhoneNumber(String phoneNumber) { + return phoneNumber != null && phoneNumber.matches("\\d{10,11}"); + } +} \ No newline at end of file diff --git a/src/main/java/com/danahub/zipitda/common/dto/CommonResponse.java b/src/main/java/com/danahub/zipitda/common/dto/CommonResponse.java new file mode 100644 index 0000000..574dd49 --- /dev/null +++ b/src/main/java/com/danahub/zipitda/common/dto/CommonResponse.java @@ -0,0 +1,27 @@ +package com.danahub.zipitda.common.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.ibatis.annotations.ConstructorArgs; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CommonResponse { + private int code; // 에러 코드 + private String message; // 메시지 + private T data; // 응답 데이터 + + // 성공 응답용 메서드 + public static CommonResponse success(T data) { + return new CommonResponse<>(0, "Success", data); + } + + // 에러 응답용 메서드 + public static CommonResponse error(int code, String message) { + return new CommonResponse<>(code, message, null); + } +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 0d4bce7..769db2d 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -26,6 +26,8 @@ - + + + \ No newline at end of file