diff --git a/build.gradle b/build.gradle index 598523e..73ae56b 100644 --- a/build.gradle +++ b/build.gradle @@ -37,13 +37,23 @@ dependencies { // AWS S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - + + // Mail + implementation 'com.sun.mail:jakarta.mail:2.0.1' + implementation 'javax.activation:activation:1.1.1' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.3' + + compileOnly 'org.projectlombok:lombok' implementation 'org.postgresql:postgresql:42.7.3' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1' } tasks.named('test') { diff --git a/src/main/java/dgu/aecofarm/domain/email/service/EmailService.java b/src/main/java/dgu/aecofarm/domain/email/service/EmailService.java new file mode 100644 index 0000000..41e7b1f --- /dev/null +++ b/src/main/java/dgu/aecofarm/domain/email/service/EmailService.java @@ -0,0 +1,8 @@ +package dgu.aecofarm.domain.email.service; + +import dgu.aecofarm.entity.EmailMessage; + +public interface EmailService { + void sendAuthCode(String email, String authCode); +} + diff --git a/src/main/java/dgu/aecofarm/domain/email/service/EmailServiceImpl.java b/src/main/java/dgu/aecofarm/domain/email/service/EmailServiceImpl.java new file mode 100644 index 0000000..7035bee --- /dev/null +++ b/src/main/java/dgu/aecofarm/domain/email/service/EmailServiceImpl.java @@ -0,0 +1,32 @@ +package dgu.aecofarm.domain.email.service; + +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; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmailServiceImpl implements EmailService { + + private final JavaMailSender javaMailSender; + + @Override + public void sendAuthCode(String email, String authCode) { + try { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); + mimeMessageHelper.setTo(email); + mimeMessageHelper.setSubject("[AECOfarm] Email Verification Code"); + mimeMessageHelper.setText("Your verification code is: " + authCode, false); // HTML이 아니라면 false로 설정 + javaMailSender.send(mimeMessage); + log.info("Auth code sent to email: " + email); + } catch (jakarta.mail.MessagingException e) { + log.error("Failed to send auth code to email: " + email, e); + throw new RuntimeException("Failed to send auth code", e); + } + } +} diff --git a/src/main/java/dgu/aecofarm/domain/email/service/UserService.java b/src/main/java/dgu/aecofarm/domain/email/service/UserService.java new file mode 100644 index 0000000..425abd5 --- /dev/null +++ b/src/main/java/dgu/aecofarm/domain/email/service/UserService.java @@ -0,0 +1,41 @@ +package dgu.aecofarm.domain.email.service; + +import dgu.aecofarm.entity.Member; +import dgu.aecofarm.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserService { + private final MemberRepository memberRepository; + + // 임시 비밀번호를 설정하는 메서드 + public void setTempPassword(String email, String tempPassword) { + Optional optionalMember = memberRepository.findMemberByEmail(email); + if (!optionalMember.isPresent()) { + throw new IllegalArgumentException("유효한 이메일이 아닙니다."); + } + Member member = optionalMember.get(); + member.updatePassword(tempPassword); + memberRepository.save(member); + } + + // 사용자 정보 조회 메서드 + public Optional getMemberByEmail(String email) { + return memberRepository.findMemberByEmail(email); + } + + // 사용자 비밀번호 재설정 메서드 + public void resetPassword(String email, String newPassword) { + Optional optionalMember = memberRepository.findMemberByEmail(email); + if (!optionalMember.isPresent()) { + throw new IllegalArgumentException("유효한 이메일이 아닙니다."); + } + Member member = optionalMember.get(); + member.updatePassword(newPassword); + memberRepository.save(member); + } +} \ No newline at end of file diff --git a/src/main/java/dgu/aecofarm/domain/member/controller/MemberController.java b/src/main/java/dgu/aecofarm/domain/member/controller/MemberController.java index 398f16f..9d06f36 100644 --- a/src/main/java/dgu/aecofarm/domain/member/controller/MemberController.java +++ b/src/main/java/dgu/aecofarm/domain/member/controller/MemberController.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dgu.aecofarm.domain.member.service.MemberService; +import dgu.aecofarm.dto.email.EmailResponseDto; import dgu.aecofarm.dto.member.*; import dgu.aecofarm.entity.Member; import dgu.aecofarm.entity.Response; @@ -28,12 +29,24 @@ public Response signup(@RequestPart("signupData") SignupRequestDTO signupRequ @RequestPart("file") MultipartFile file) { try { String imageUrl = memberService.uploadFile(file); - return Response.success(memberService.signup(signupRequestDTO, imageUrl)); + SignupResponseDTO signupResponseDTO = memberService.initiateSignup(signupRequestDTO, imageUrl); + signupResponseDTO.getSignupRequestDTO().setImageUrl(imageUrl); + return Response.success(signupResponseDTO); + } catch (Exception e) { + return Response.failure(e); + } + } + @PostMapping("/signup/complete") + public Response completeSignup(@RequestBody SignupCompleteDTO signupCompleteDTO) { + try { + return Response.success(memberService.completeSignup(signupCompleteDTO.getSignupRequestDTO(), + signupCompleteDTO.getAuthCode(), + signupCompleteDTO.getExpectedCode(), + signupCompleteDTO.getSignupRequestDTO().getImageUrl())); } catch (Exception e) { return Response.failure(e); } } - @PostMapping("/login") public Response login(@RequestBody LoginRequestDTO loginRequestDTO) { try { diff --git a/src/main/java/dgu/aecofarm/domain/member/service/MemberService.java b/src/main/java/dgu/aecofarm/domain/member/service/MemberService.java index 7bf7aba..d1daf44 100644 --- a/src/main/java/dgu/aecofarm/domain/member/service/MemberService.java +++ b/src/main/java/dgu/aecofarm/domain/member/service/MemberService.java @@ -7,7 +7,9 @@ import java.util.Optional; public interface MemberService { - String signup(SignupRequestDTO registerRequestDTO, String imageUrl); + SignupResponseDTO initiateSignup(SignupRequestDTO signupRequestDTO, String imageUrl); + + String completeSignup(SignupRequestDTO signupRequestDTO, String authCode, String expectedCode, String imageUrl); String uploadFile(MultipartFile file); diff --git a/src/main/java/dgu/aecofarm/domain/member/service/MemberServiceImpl.java b/src/main/java/dgu/aecofarm/domain/member/service/MemberServiceImpl.java index 1841aca..f278807 100644 --- a/src/main/java/dgu/aecofarm/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/dgu/aecofarm/domain/member/service/MemberServiceImpl.java @@ -4,6 +4,7 @@ import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.fasterxml.jackson.databind.ObjectMapper; +import dgu.aecofarm.domain.email.service.EmailService; import dgu.aecofarm.dto.member.*; import dgu.aecofarm.entity.*; import dgu.aecofarm.exception.InvalidUserIdException; @@ -35,6 +36,7 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; + private final EmailService emailService; private final ItemRepository itemRepository; private final ObjectMapper objectMapper; private final ContractRepository contractRepository; @@ -45,24 +47,34 @@ public class MemberServiceImpl implements MemberService { @Value("${cloud.aws.s3.bucket}") private String bucketName; - public String signup(SignupRequestDTO signupRequestDTO, String imageUrl) { - String email = signupRequestDTO.getEmail(); - String userName = signupRequestDTO.getUserName(); - String password = toSHA256(signupRequestDTO.getPassword()); - String phone = signupRequestDTO.getPhone(); - int schoolNum = signupRequestDTO.getSchoolNum(); - - Member checkDuplicate = memberRepository.findMemberByEmail(email); - if (checkDuplicate != null) { + @Override + public SignupResponseDTO initiateSignup(SignupRequestDTO signupRequestDTO, String imageUrl) { + Optional checkDuplicate = memberRepository.findMemberByEmail(signupRequestDTO.getEmail()); + if (checkDuplicate.isPresent()) { throw new IllegalArgumentException("중복된 이메일입니다."); } + String authCode = generateAuthCode(); + emailService.sendAuthCode(signupRequestDTO.getEmail(), authCode); + + return SignupResponseDTO.builder() + .signupRequestDTO(signupRequestDTO) + .expectedCode(authCode) + .build(); + } + + @Override + public String completeSignup(SignupRequestDTO signupRequestDTO, String authCode, String expectedCode, String imageUrl) { + if (!authCode.equals(expectedCode)) { + throw new IllegalArgumentException("인증 코드가 일치하지 않습니다."); + } + Member member = Member.builder() - .email(email) - .userName(userName) - .password(password) - .phone(phone) - .schoolNum(schoolNum) + .email(signupRequestDTO.getEmail()) + .userName(signupRequestDTO.getUserName()) + .password(toSHA256(signupRequestDTO.getPassword())) + .phone(signupRequestDTO.getPhone()) + .schoolNum(signupRequestDTO.getSchoolNum()) .image(imageUrl) .point(3000) .build(); @@ -73,6 +85,7 @@ public String signup(SignupRequestDTO signupRequestDTO, String imageUrl) { return "회원가입 성공"; } + @Override public String uploadFile(MultipartFile file) { if (file.isEmpty()) { return null; @@ -94,12 +107,14 @@ public String uploadFile(MultipartFile file) { } } + @Override public LoginResponseDTO login(LoginRequestDTO loginRequestDTO) { - Member member = memberRepository.findMemberByEmail(loginRequestDTO.getEmail()); - if (member == null) { + Optional optionalMember = memberRepository.findMemberByEmail(loginRequestDTO.getEmail()); + if (!optionalMember.isPresent()) { throw new IllegalArgumentException("유효한 이메일이 아닙니다."); } + Member member = optionalMember.get(); String encode_password = toSHA256(loginRequestDTO.getPassword()); if (encode_password.equals(member.getPassword())) { @@ -111,13 +126,13 @@ public LoginResponseDTO login(LoginRequestDTO loginRequestDTO) { } throw new IllegalArgumentException("비밀번호가 틀립니다."); } - // Jwt Token에서 추출한 loginId로 Member 찾아오기 + @Override public Optional getLoginUserInfoByMemberId(String memberId) { return memberRepository.findByMemberId(Long.valueOf(memberId)); } - // 로그인한 Member 조회 + @Override public Optional getLoginUserInfoByUserid(String memberId) { return memberRepository.findByMemberId(Long.valueOf(memberId)).map(member -> JwtInfoResponseDTO.builder() @@ -126,28 +141,27 @@ public Optional getLoginUserInfoByUserid(String memberId) { .build()); } + @Override public String findPassword(FindPasswordRequestDTO findPasswordDTO) { - // memberId로 회원 정보 조회 - Member member = memberRepository.findMemberByEmail(findPasswordDTO.getEmail()); - - if (member == null) { + Optional optionalMember = memberRepository.findMemberByEmail(findPasswordDTO.getEmail()); + if (!optionalMember.isPresent()) { throw new IllegalArgumentException("유효한 이메일이 아닙니다."); - } + }// 사용자 이름과 학번이 일치하는지 확인 + Member member = optionalMember.get(); // 사용자 이름과 학번이 일치하는지 확인 if (!member.getUserName().equals(findPasswordDTO.getUserName()) || member.getSchoolNum() != findPasswordDTO.getSchoolNum()) { throw new IllegalArgumentException("사용자 정보가 일치하지 않습니다."); } - // 비밀번호 재설정 String newPassword = toSHA256(findPasswordDTO.getPassword()); member.updatePassword(newPassword); memberRepository.save(member); return "비밀번호 재설정에 성공하였습니다."; - } + @Override public String signout(String memberId) { Member member = memberRepository.findById(Long.valueOf(memberId)) .orElseThrow(() -> new IllegalArgumentException("유효한 사용자 ID가 아닙니다.")); @@ -160,24 +174,7 @@ public String signout(String memberId) { return "회원 탈퇴에 성공하였습니다."; } - private String toSHA256(String base) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(base.getBytes("UTF-8")); - StringBuilder hexString = new StringBuilder(); - - for (int i = 0; i < hash.length; i++) { - String hex = Integer.toHexString(0xff & hash[i]); - if(hex.length() == 1) hexString.append('0'); - hexString.append(hex); - } - - return hexString.toString(); - } catch(NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - + @Override public RecommendResponseDTO getRecommand(String memberId) { Member member = memberRepository.findById(Long.valueOf(memberId)) .orElseThrow(() -> new InvalidUserIdException("유효한 사용자 ID가 아닙니다.")); @@ -226,6 +223,7 @@ public RecommendResponseDTO getRecommand(String memberId) { .build(); } + @Override public SearchResponseDTO searchItems(SearchRequestDTO searchRequestDTO, String memberId) { Member member = memberRepository.findById(Long.valueOf(memberId)) .orElseThrow(() -> new InvalidUserIdException("유효한 사용자 ID가 아닙니다.")); @@ -290,4 +288,26 @@ public SearchResponseDTO searchItems(SearchRequestDTO searchRequestDTO, String m .borrowItems(borrowItems) .build(); } + + private String generateAuthCode() { + return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); + } + + private String toSHA256(String base) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(base.getBytes("UTF-8")); + StringBuilder hexString = new StringBuilder(); + + for (int i = 0; i < hash.length; i++) { + String hex = Integer.toHexString(0xff & hash[i]); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + + return hexString.toString(); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/dgu/aecofarm/dto/email/EmailPostDto.java b/src/main/java/dgu/aecofarm/dto/email/EmailPostDto.java new file mode 100644 index 0000000..0fc2097 --- /dev/null +++ b/src/main/java/dgu/aecofarm/dto/email/EmailPostDto.java @@ -0,0 +1,8 @@ +package dgu.aecofarm.dto.email; + +import lombok.Getter; + +@Getter +public class EmailPostDto { + private String email; +} diff --git a/src/main/java/dgu/aecofarm/dto/email/EmailResponseDto.java b/src/main/java/dgu/aecofarm/dto/email/EmailResponseDto.java new file mode 100644 index 0000000..ef8e0ea --- /dev/null +++ b/src/main/java/dgu/aecofarm/dto/email/EmailResponseDto.java @@ -0,0 +1,10 @@ +package dgu.aecofarm.dto.email; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class EmailResponseDto { + private String code; +} diff --git a/src/main/java/dgu/aecofarm/dto/member/SignupCompleteDTO.java b/src/main/java/dgu/aecofarm/dto/member/SignupCompleteDTO.java new file mode 100644 index 0000000..954c26b --- /dev/null +++ b/src/main/java/dgu/aecofarm/dto/member/SignupCompleteDTO.java @@ -0,0 +1,12 @@ +package dgu.aecofarm.dto.member; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SignupCompleteDTO { + private SignupRequestDTO signupRequestDTO; + private String authCode; + private String expectedCode; +} diff --git a/src/main/java/dgu/aecofarm/dto/member/SignupRequestDTO.java b/src/main/java/dgu/aecofarm/dto/member/SignupRequestDTO.java index 0488ac1..f27f28d 100644 --- a/src/main/java/dgu/aecofarm/dto/member/SignupRequestDTO.java +++ b/src/main/java/dgu/aecofarm/dto/member/SignupRequestDTO.java @@ -1,12 +1,15 @@ package dgu.aecofarm.dto.member; +import lombok.Builder; import lombok.Data; @Data +@Builder public class SignupRequestDTO { private String email; private String userName; private String password; private String phone; private int schoolNum; + private String imageUrl; } diff --git a/src/main/java/dgu/aecofarm/dto/member/SignupResponseDTO.java b/src/main/java/dgu/aecofarm/dto/member/SignupResponseDTO.java new file mode 100644 index 0000000..de61f22 --- /dev/null +++ b/src/main/java/dgu/aecofarm/dto/member/SignupResponseDTO.java @@ -0,0 +1,11 @@ +package dgu.aecofarm.dto.member; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SignupResponseDTO { + private SignupRequestDTO signupRequestDTO; + private String expectedCode; +} \ No newline at end of file diff --git a/src/main/java/dgu/aecofarm/entity/EmailMessage.java b/src/main/java/dgu/aecofarm/entity/EmailMessage.java new file mode 100644 index 0000000..04c9fdc --- /dev/null +++ b/src/main/java/dgu/aecofarm/entity/EmailMessage.java @@ -0,0 +1,15 @@ +package dgu.aecofarm.entity; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class EmailMessage { + + private String to; + private String subject; + private String message; +} diff --git a/src/main/java/dgu/aecofarm/repository/MemberRepository.java b/src/main/java/dgu/aecofarm/repository/MemberRepository.java index eb2043c..41f707c 100644 --- a/src/main/java/dgu/aecofarm/repository/MemberRepository.java +++ b/src/main/java/dgu/aecofarm/repository/MemberRepository.java @@ -8,7 +8,7 @@ @Repository public interface MemberRepository extends JpaRepository { - Member findMemberByEmail(String email); + Optional findMemberByEmail(String email); Optional findByMemberId(Long memberId); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7d5f86f..cff9e83 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,20 @@ spring: username: ${DB_USER_NAME} password: ${DB_PASSWORD} + mail: + host: ${MAIL_HOST} + port: ${MAIL_PORT} + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + timeout: 5000 + starttls: + enable: true + debug: true + cloud: aws: s3: @@ -23,4 +37,4 @@ cloud: region: static: ap-northeast-2 stack: - auto: false \ No newline at end of file + auto: false diff --git a/src/main/resources/templates/email.html b/src/main/resources/templates/email.html new file mode 100644 index 0000000..ab3467b --- /dev/null +++ b/src/main/resources/templates/email.html @@ -0,0 +1,20 @@ + + + + +
+

안녕하세요.

+

대학생을 위한 물품 대여 플랫폼 aecofarm 입니다.

+
+

아래 코드를 회원가입 창으로 돌아가 입력해주세요.

+
+ +
+

회원가입 인증 코드 입니다.

+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/password.html b/src/main/resources/templates/password.html new file mode 100644 index 0000000..bfbd8ca --- /dev/null +++ b/src/main/resources/templates/password.html @@ -0,0 +1,21 @@ + + + + +
+

안녕하세요.

+

대학생을 위한 물품 대여 플랫폼 aecofarm 입니다.

+
+

임시 비밀번호를 발급드립니다. 아래 발급된 비밀번호로 로그인해주세요.

+
+ +
+

임시 비밀번호 입니다.

+
+
+
+
+ + + + \ No newline at end of file