diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d4782c7..b4fc17d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,7 +25,6 @@ jobs: - name: Set up Environment working-directory: ./src/main run: | - mkdir ./resources cd ./resources touch ./application.yml echo "${{ secrets.APPLICATION_YML }}" | base64 --decode >> application.yml diff --git a/build.gradle b/build.gradle index 79e9b5e..1652651 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,12 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'com.auth0:java-jwt:4.4.0' + + // SMTP 설정 + implementation 'org.springframework.boot:spring-boot-starter-mail' + + // Thymeleaf 설정 + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' } tasks.named('test') { diff --git a/src/main/java/com/fullcar/carpool/presentation/carpool/dto/response/CarpoolResponseDto.java b/src/main/java/com/fullcar/carpool/presentation/carpool/dto/response/CarpoolResponseDto.java index c679f95..e7e0287 100644 --- a/src/main/java/com/fullcar/carpool/presentation/carpool/dto/response/CarpoolResponseDto.java +++ b/src/main/java/com/fullcar/carpool/presentation/carpool/dto/response/CarpoolResponseDto.java @@ -3,6 +3,7 @@ import com.fullcar.carpool.domain.carpool.CarpoolState; import com.fullcar.carpool.domain.carpool.MoodType; import com.fullcar.carpool.domain.carpool.PeriodType; +import com.fullcar.member.domain.member.Gender; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -45,7 +46,7 @@ public class CarpoolResponseDto { private String companyName; @Schema(description = "성별") - private String gender; + private Gender gender; @Schema(description = "생성 Timestamp") private LocalDateTime createdAt; diff --git a/src/main/java/com/fullcar/core/response/ErrorCode.java b/src/main/java/com/fullcar/core/response/ErrorCode.java index d429428..a7236fb 100644 --- a/src/main/java/com/fullcar/core/response/ErrorCode.java +++ b/src/main/java/com/fullcar/core/response/ErrorCode.java @@ -13,6 +13,7 @@ public enum ErrorCode { /* 400 BAD REQUEST */ FAILED_TO_GENERATE_PUBLIC_KEY(BAD_REQUEST, "애플 공개키 생성 중 문제 발생"), + EMAIL_ADDRESS_IN_BLACKLIST(BAD_REQUEST, "블랙리스트에 있는 이메일 주소입니다."), /* 401 UNAUTHORIZED */ UNAUTHORIZED_KAKAO_TOKEN(UNAUTHORIZED, "유효하지 않은 카카오 토큰"), diff --git a/src/main/java/com/fullcar/core/response/SuccessCode.java b/src/main/java/com/fullcar/core/response/SuccessCode.java index e45edea..1681ae5 100644 --- a/src/main/java/com/fullcar/core/response/SuccessCode.java +++ b/src/main/java/com/fullcar/core/response/SuccessCode.java @@ -14,7 +14,8 @@ public enum SuccessCode { /* 200 OK */ SIGNIN_SUCCESS(OK, "소셜로그인 성공"), GET_NEW_TOKEN_SUCCESS(OK, "토큰 재발급 성공"), - READ_SUCCESS(OK, "조회 성공"); + READ_SUCCESS(OK, "조회 성공"), + EMAIL_SENT_SUCCESS(OK, "인증메일 발송 성공"); private final HttpStatus status; private final String message; diff --git a/src/main/java/com/fullcar/member/application/member/MemberMapper.java b/src/main/java/com/fullcar/member/application/member/MemberMapper.java index 31363d0..497c778 100644 --- a/src/main/java/com/fullcar/member/application/member/MemberMapper.java +++ b/src/main/java/com/fullcar/member/application/member/MemberMapper.java @@ -1,6 +1,8 @@ package com.fullcar.member.application.member; +import com.fullcar.member.infra.EmailMessage; import com.fullcar.member.domain.member.Member; +import com.fullcar.member.presentation.member.dto.request.EmailRequestDto; import com.fullcar.member.presentation.member.dto.response.MemberGetResponseDto; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -14,6 +16,16 @@ public MemberGetResponseDto toDto(Member member) { return MemberGetResponseDto.builder() .nickname(member.getNickname()) .companyName(member.getCompany().getCompanyName()) + .email(member.getEmail()) + .gender(member.getGender()) + .carId(member.getCarId()) + .build(); + } + + public EmailMessage toEntity(EmailRequestDto emailRequestDto) { + return EmailMessage.builder() + .to(emailRequestDto.getEmail()) + .subject("[FullCar] 회사 이메일 인증") .build(); } } diff --git a/src/main/java/com/fullcar/member/domain/blacklist/Blacklist.java b/src/main/java/com/fullcar/member/domain/blacklist/Blacklist.java new file mode 100644 index 0000000..2b3e942 --- /dev/null +++ b/src/main/java/com/fullcar/member/domain/blacklist/Blacklist.java @@ -0,0 +1,25 @@ +package com.fullcar.member.domain.blacklist; + +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import static lombok.AccessLevel.PROTECTED; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor(access = PROTECTED) +@EntityListeners(AuditingEntityListener.class) +@Builder +public class Blacklist { + @EmbeddedId + private BlacklistId id; + + private String email; +} diff --git a/src/main/java/com/fullcar/member/domain/blacklist/BlacklistId.java b/src/main/java/com/fullcar/member/domain/blacklist/BlacklistId.java new file mode 100644 index 0000000..5c8e480 --- /dev/null +++ b/src/main/java/com/fullcar/member/domain/blacklist/BlacklistId.java @@ -0,0 +1,21 @@ +package com.fullcar.member.domain.blacklist; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +import static lombok.AccessLevel.PROTECTED; + +@Embeddable +@Getter +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class BlacklistId implements Serializable { + + @Column(name = "id") + private Long id; +} diff --git a/src/main/java/com/fullcar/member/domain/blacklist/BlacklistRepository.java b/src/main/java/com/fullcar/member/domain/blacklist/BlacklistRepository.java new file mode 100644 index 0000000..c0129ba --- /dev/null +++ b/src/main/java/com/fullcar/member/domain/blacklist/BlacklistRepository.java @@ -0,0 +1,17 @@ +package com.fullcar.member.domain.blacklist; + +import com.fullcar.core.exception.CustomException; +import com.fullcar.core.response.ErrorCode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BlacklistRepository extends JpaRepository { + boolean existsByEmail(String email); + + default void findByEmailThrow(String email) { + if (existsByEmail(email)) { + throw new CustomException(ErrorCode.EMAIL_ADDRESS_IN_BLACKLIST); + } + } +} diff --git a/src/main/java/com/fullcar/member/domain/blacklist/service/BlacklistIdService.java b/src/main/java/com/fullcar/member/domain/blacklist/service/BlacklistIdService.java new file mode 100644 index 0000000..e70cb81 --- /dev/null +++ b/src/main/java/com/fullcar/member/domain/blacklist/service/BlacklistIdService.java @@ -0,0 +1,18 @@ +package com.fullcar.member.domain.blacklist.service; + +import com.fullcar.core.id.SnowFlake; +import com.fullcar.member.domain.blacklist.BlacklistId; +import org.springframework.stereotype.Service; + +@Service +public class BlacklistIdService { + private final SnowFlake snowFlake; + + public BlacklistIdService() { + snowFlake = new SnowFlake(255); + } + + public BlacklistId nextId() { + return new BlacklistId(snowFlake.nextId()); + } +} diff --git a/src/main/java/com/fullcar/member/domain/member/Gender.java b/src/main/java/com/fullcar/member/domain/member/Gender.java new file mode 100644 index 0000000..fa7ca4c --- /dev/null +++ b/src/main/java/com/fullcar/member/domain/member/Gender.java @@ -0,0 +1,7 @@ +package com.fullcar.member.domain.member; + +public enum Gender { + FEMALE, + MALE, + NONE +} diff --git a/src/main/java/com/fullcar/member/domain/member/Member.java b/src/main/java/com/fullcar/member/domain/member/Member.java index db714b1..74e7fa2 100644 --- a/src/main/java/com/fullcar/member/domain/member/Member.java +++ b/src/main/java/com/fullcar/member/domain/member/Member.java @@ -43,7 +43,7 @@ public class Member { private String email; - private String gender; + private Gender gender; @Builder.Default @Column(name = "onboarding_flag") diff --git a/src/main/java/com/fullcar/member/domain/member/service/MailService.java b/src/main/java/com/fullcar/member/domain/member/service/MailService.java new file mode 100644 index 0000000..25f5dff --- /dev/null +++ b/src/main/java/com/fullcar/member/domain/member/service/MailService.java @@ -0,0 +1,9 @@ +package com.fullcar.member.domain.member.service; + +import com.fullcar.member.presentation.member.dto.request.EmailRequestDto; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public interface MailService { + void sendMail(EmailRequestDto emailRequestDto); +} diff --git a/src/main/java/com/fullcar/member/infra/EmailMessage.java b/src/main/java/com/fullcar/member/infra/EmailMessage.java new file mode 100644 index 0000000..6ced23e --- /dev/null +++ b/src/main/java/com/fullcar/member/infra/EmailMessage.java @@ -0,0 +1,11 @@ +package com.fullcar.member.infra; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class EmailMessage { + private String to; + private String subject; +} diff --git a/src/main/java/com/fullcar/member/infra/MailClient.java b/src/main/java/com/fullcar/member/infra/MailClient.java new file mode 100644 index 0000000..2e3801d --- /dev/null +++ b/src/main/java/com/fullcar/member/infra/MailClient.java @@ -0,0 +1,50 @@ +package com.fullcar.member.infra; + +import com.fullcar.member.application.member.MemberMapper; +import com.fullcar.member.domain.blacklist.BlacklistRepository; +import com.fullcar.member.domain.member.service.MailService; +import com.fullcar.member.presentation.member.dto.request.EmailRequestDto; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +@Service +@RequiredArgsConstructor +public class MailClient implements MailService { + private final BlacklistRepository blacklistRepository; + private final JavaMailSender javaMailSender; + private final SpringTemplateEngine templateEngine; + private final MemberMapper memberMapper; + + @Override + public void sendMail(EmailRequestDto emailRequestDto) { + String email = emailRequestDto.getEmail(); + String emailDomain = email.substring(email.lastIndexOf("@")+1); + blacklistRepository.findByEmailThrow(emailDomain); + + EmailMessage emailMessage = memberMapper.toEntity(emailRequestDto); + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + + try { + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + mimeMessageHelper.setTo(emailMessage.getTo()); + mimeMessageHelper.setSubject(emailMessage.getSubject()); + mimeMessageHelper.setText(setContext(),true); + mimeMessageHelper.addInline("image", new ClassPathResource("static/images/fullcar_logo.png")); + javaMailSender.send(mimeMessage); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + public String setContext() { + Context context = new Context(); + return templateEngine.process("email", context); + } +} diff --git a/src/main/java/com/fullcar/member/presentation/member/MemberController.java b/src/main/java/com/fullcar/member/presentation/member/MemberController.java index 17a696c..5163892 100644 --- a/src/main/java/com/fullcar/member/presentation/member/MemberController.java +++ b/src/main/java/com/fullcar/member/presentation/member/MemberController.java @@ -5,7 +5,9 @@ import com.fullcar.core.response.SuccessCode; import com.fullcar.member.application.member.MemberService; import com.fullcar.member.domain.member.Member; +import com.fullcar.member.domain.member.service.MailService; import com.fullcar.member.presentation.member.dto.request.CompanyRequestDto; +import com.fullcar.member.presentation.member.dto.request.EmailRequestDto; import com.fullcar.member.presentation.member.dto.response.MemberGetResponseDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -22,6 +24,7 @@ public class MemberController { private final MemberService memberService; + private final MailService mailService; @Operation(summary = "회사 선택 API") @ApiResponses({ @@ -44,4 +47,15 @@ public ApiResponse getMember(@CurrentMember Member member) MemberGetResponseDto response = memberService.getMember(member); return ApiResponse.success(SuccessCode.READ_SUCCESS, response); } + + @Operation(summary = "회사 메일 인증 API") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "인증메일 발송 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content) + }) + @PostMapping("/onboarding/company/email") + public ApiResponse sendAuthenticationMail(@RequestBody EmailRequestDto emailRequestDto) { + mailService.sendMail(emailRequestDto); + return ApiResponse.success(SuccessCode.EMAIL_SENT_SUCCESS); + } } diff --git a/src/main/java/com/fullcar/member/presentation/member/dto/request/EmailRequestDto.java b/src/main/java/com/fullcar/member/presentation/member/dto/request/EmailRequestDto.java new file mode 100644 index 0000000..1aeed3f --- /dev/null +++ b/src/main/java/com/fullcar/member/presentation/member/dto/request/EmailRequestDto.java @@ -0,0 +1,15 @@ +package com.fullcar.member.presentation.member.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EmailRequestDto { + @Schema(description = "회사 이메일", example = "whoareyou@yanolja.com") + private String email; +} diff --git a/src/main/java/com/fullcar/member/presentation/member/dto/response/MemberGetResponseDto.java b/src/main/java/com/fullcar/member/presentation/member/dto/response/MemberGetResponseDto.java index 91df648..70f34fb 100644 --- a/src/main/java/com/fullcar/member/presentation/member/dto/response/MemberGetResponseDto.java +++ b/src/main/java/com/fullcar/member/presentation/member/dto/response/MemberGetResponseDto.java @@ -1,5 +1,7 @@ package com.fullcar.member.presentation.member.dto.response; +import com.fullcar.member.domain.car.CarId; +import com.fullcar.member.domain.member.Gender; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; @@ -15,4 +17,14 @@ public class MemberGetResponseDto { @Schema(description = "회사명", example = "현대 자동차") private String companyName; + + @Schema(description = "회사 이메일", example = "whoareyou@yanolja.com") + private String email; + + @Schema(description = "성별", example = "MALE") + private Gender gender; + + @Schema(description = "차량 id") + private CarId carId; + } diff --git a/src/main/resources/static/images/fullcar_logo.png b/src/main/resources/static/images/fullcar_logo.png new file mode 100644 index 0000000..eb3b976 Binary files /dev/null and b/src/main/resources/static/images/fullcar_logo.png differ diff --git a/src/main/resources/templates/email.html b/src/main/resources/templates/email.html new file mode 100644 index 0000000..04fdfbd --- /dev/null +++ b/src/main/resources/templates/email.html @@ -0,0 +1,40 @@ + + + + + + + + +
+ +

안녕하세요.


+

회사 메일 인증을 통한 안전한 카풀 서비스 풀카입니다.

+

풀카 이용을 위해 아래 버튼을 클릭해 인증을 진행해 주세요.

+ + + + + +
+ + + +