Skip to content

Commit

Permalink
Merge pull request #81 from UMC-TripPiece/feature/79
Browse files Browse the repository at this point in the history
  • Loading branch information
yyypearl authored Nov 17, 2024
2 parents 6e2f179 + 020adf4 commit 4be7331
Show file tree
Hide file tree
Showing 15 changed files with 470 additions and 8 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation('org.projectlombok:lombok')
/* Email 전송 관련 */
implementation 'org.springframework.boot:spring-boot-starter-mail:3.1.2'

/* 메일 HTML 작성 관련 */
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}

tasks.named('test') {
Expand Down
73 changes: 73 additions & 0 deletions src/main/java/umc/TripPiece/config/EmailConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package umc.TripPiece.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import umc.TripPiece.service.EmailService;

import java.util.Properties;

@Configuration
public class EmailConfig {
@Value("${spring.mail.host}")
private String host;

@Value("${spring.mail.port}")
private int port;

@Value("${spring.mail.username}")
private String username;

@Value("${spring.mail.password}")
private String password;

@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;

@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttlsEnable;

@Value("${spring.mail.properties.mail.smtp.starttls.required}")
private boolean starttlsRequired;

@Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
private int connectionTimeout;

@Value("${spring.mail.properties.mail.smtp.timeout}")
private int timeout;

@Value("${spring.mail.properties.mail.smtp.writetimeout}")
private int writeTimeout;

@Bean
public EmailService emailService() {
return new EmailService(javaMailSender());
}

@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());

return mailSender;
}

private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", auth);
properties.put("mail.smtp.starttls.enable", starttlsEnable);
properties.put("mail.smtp.starttls.required", starttlsRequired);
properties.put("mail.smtp.connectiontimeout", connectionTimeout);
properties.put("mail.smtp.timeout", timeout);
properties.put("mail.smtp.writetimeout", writeTimeout);

return properties;
}
}
31 changes: 31 additions & 0 deletions src/main/java/umc/TripPiece/domain/VerificationCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package umc.TripPiece.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@Entity
public class VerificationCode {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String email;
private String code;
private LocalDateTime expirationTime;

public VerificationCode(String email, String code, int expirationMinutes) {
this.email = email;
this.code = code;
this.expirationTime = LocalDateTime.now().plusMinutes(expirationMinutes);
}

public boolean isExpired() {
return LocalDateTime.now().isAfter(expirationTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package umc.TripPiece.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import umc.TripPiece.domain.VerificationCode;

import java.util.Optional;

@Repository
public interface VerificationCodeRepository extends JpaRepository<VerificationCode, Long> {
Optional<VerificationCode> findTopByEmailOrderByExpirationTimeDesc(String email);
}
68 changes: 68 additions & 0 deletions src/main/java/umc/TripPiece/service/EmailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package umc.TripPiece.service;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Random;

@Slf4j
@Transactional
@Service
@RequiredArgsConstructor
public class EmailService {

private final JavaMailSender emailSender;

public String generateVerificationCode() {
Random random = new Random();
int code = 100000 + random.nextInt(900000); // 6자리 숫자 생성
return String.valueOf(code);
}

public void sendVerificationCode(String toEmail, String code) throws MessagingException, IOException {
String subject = "[여행조각(TripPiece)] 회원가입 시 이메일 인증번호 안내드립니다.";

// HTML 파일을 읽고 코드 삽입
String content;
try {
content = getEmailHtmlContent(code);
} catch (IOException e) {
log.error("Failed to read email template", e);
throw new MessagingException("이메일 템플릿을 읽는 중 오류가 발생했습니다.");
}

sendEmail(toEmail, subject, content);
}

private String getEmailHtmlContent(String code) throws IOException {
String htmlTemplatePath = "src/main/resources/templates/email.html";
String content = new String(Files.readAllBytes(Paths.get(htmlTemplatePath)));

// 인증 코드 삽입
content = content.replace("{code}", code);

return content;
}

public void sendEmail(String toEmail, String title, String content) throws MessagingException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(toEmail);
helper.setSubject(title);
helper.setText(content, true);
try {
emailSender.send(message);
} catch (RuntimeException e) {
throw new RuntimeException("이메일 전송 불가", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package umc.TripPiece.web.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import umc.TripPiece.payload.ApiResponse;
import umc.TripPiece.service.CityService;
Expand All @@ -19,6 +16,7 @@

import java.util.List;

@Tag(name = "City", description = "도시 관련 API")
@RestController
@RequiredArgsConstructor
public class CityController {
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/umc/TripPiece/web/controller/EmailController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package umc.TripPiece.web.controller;

import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.mail.MessagingException;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import umc.TripPiece.domain.VerificationCode;
import umc.TripPiece.payload.ApiResponse;
import umc.TripPiece.repository.VerificationCodeRepository;
import umc.TripPiece.service.EmailService;
import umc.TripPiece.web.dto.request.EmailRequestDto;

import java.io.IOException;
import java.util.Optional;

@Tag(name = "Email", description = "이메일 인증 관련 API")
@RestController
@RequestMapping("/email")
@RequiredArgsConstructor
public class EmailController {

private final EmailService emailService;
private final VerificationCodeRepository verificationCodeRepository;

@PostMapping("/send")
@Operation(summary = "이메일 인증번호 전송 API",
description = "이메일로 6자리 인증번호 발송")
public ResponseEntity<ApiResponse<String>> sendVerificationCode(@RequestBody EmailRequestDto.SendCodeDto request) {
String email = request.getEmail();

// 이메일 주소 유효성 체크
if (!isValidEmail(email)) {
return ResponseEntity.badRequest().body(ApiResponse.onFailure("400", "유효한 이메일 주소여야 합니다.", null));
}

String code = emailService.generateVerificationCode();

VerificationCode verificationCode = new VerificationCode(request.getEmail(), code, 3); // 인증코드 유효시간 (3분)
verificationCodeRepository.save(verificationCode);

try {
emailService.sendVerificationCode(email, code);
} catch (MessagingException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.onFailure("500", "이메일 전송에 실패했습니다.", null));
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.onFailure("500", "이메일 템플릿을 읽는 중 오류가 발생했습니다.", null));
}

return ResponseEntity.ok(ApiResponse.onSuccess("해당 이메일로 인증번호를 전송했습니다."));
}

@PostMapping("/verify")
@Operation(summary = "이메일 인증번호 검증 API",
description = "이메일로 발송된 인증번호의 일치 여부 검증")
public ResponseEntity<ApiResponse<String>> verifyCode(@Valid @RequestBody EmailRequestDto.VerifyCodeDto request, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 오류 메시지 추출
String errorMessage = bindingResult.getFieldError().getDefaultMessage();
return ResponseEntity.badRequest().body(ApiResponse.onFailure("400", errorMessage, null));
}

Optional<VerificationCode> optionalCode = verificationCodeRepository.findTopByEmailOrderByExpirationTimeDesc(request.getEmail());

if (optionalCode.isEmpty()) {
return ResponseEntity.badRequest().body(ApiResponse.onFailure("400", "인증코드가 발송되지 않은 이메일입니다.", null));
}

VerificationCode verificationCode = optionalCode.get();


if (verificationCode.isExpired()) {
return ResponseEntity.badRequest().body(ApiResponse.onFailure("400", "이메일 인증 시간인 3분을 초과했습니다.", null));
}

if (!verificationCode.getCode().equals(request.getCode())) {
return ResponseEntity.badRequest().body(ApiResponse.onFailure("400", "인증번호가 일치하지 않습니다.", null));
}

return ResponseEntity.ok(ApiResponse.onSuccess("이메일 인증에 성공했습니다."));
}

private boolean isValidEmail(String email) {
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.(com|net|org|co\\.kr|ac\\.kr|gov|edu)$";
return email.matches(emailRegex);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.TripPiece.web.controller;

import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
Expand All @@ -21,6 +21,7 @@
import java.util.HashMap;
import java.util.Map;

@Tag(name = "Kakao", description = "카카오 유저 관련 API")
@RestController
@RequestMapping("/user/kakao")
public class KakaoController {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/umc/TripPiece/web/controller/MapController.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package umc.TripPiece.web.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import umc.TripPiece.service.MapService;
import umc.TripPiece.web.dto.request.MapRequestDto;
import umc.TripPiece.web.dto.response.ApiResponse;
Expand All @@ -13,6 +15,7 @@

import java.util.List;

@Tag(name = "Map", description = "지도 관련 API")
@RestController
@RequestMapping("/api/maps")
public class MapController {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package umc.TripPiece.web.controller;


import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -23,6 +22,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Tag(name = "Travel", description = "여행기 관련 API")
@RestController
@RequiredArgsConstructor
public class TravelController {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package umc.TripPiece.web.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -12,6 +13,7 @@

import java.util.List;

@Tag(name = "TripPiece", description = "여행 조각 관련 API")
@RestController
@RequiredArgsConstructor
public class TripPieceController {
Expand Down
Loading

0 comments on commit 4be7331

Please sign in to comment.