diff --git a/build.gradle b/build.gradle index d1b4241..318a7ef 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,12 @@ dependencies { annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + //smtp + implementation 'org.springframework.boot:spring-boot-starter-mail' + } tasks.named('test') { diff --git a/src/main/java/org/jullaene/walkmong_back/api/dog/domain/Dog.java b/src/main/java/org/jullaene/walkmong_back/api/dog/domain/Dog.java index 21055b3..bbd97c4 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/dog/domain/Dog.java +++ b/src/main/java/org/jullaene/walkmong_back/api/dog/domain/Dog.java @@ -2,21 +2,32 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.time.LocalDate; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; import org.hibernate.annotations.DynamicUpdate; import org.jullaene.walkmong_back.api.dog.domain.enums.DogSize; +import org.jullaene.walkmong_back.api.dog.dto.req.DogProfileReqDto; +import org.jullaene.walkmong_back.api.dog.dto.res.DogProfileResponseDto; import org.jullaene.walkmong_back.common.enums.Gender; import org.jullaene.walkmong_back.common.BaseEntity; @Table(name = "dog") @Entity +@NoArgsConstructor @DynamicUpdate public class Dog extends BaseEntity { @Id + @Getter @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "dog_id") private Long dogId; @@ -28,6 +39,7 @@ public class Dog extends BaseEntity { private String name; @Comment("성별") + @Enumerated(EnumType.STRING) private Gender gender; @Comment("출생년도") @@ -40,6 +52,7 @@ public class Dog extends BaseEntity { private String breed; @Comment("사이즈") + @Enumerated(EnumType.STRING) private DogSize dogSize; @Comment("프로필 url") @@ -74,4 +87,73 @@ public class Dog extends BaseEntity { @Comment("추가 안내 사항") private String additionalRequest; + + public final String getWalkRequestContent() { + return this.walkRequest; + } + + public final DogProfileResponseDto toDogProfileResponseDto() { + int currentYear = LocalDate.now().getYear(); + int dogAge = currentYear - this.birthYear + 1; // 나이 계산 + + return DogProfileResponseDto.builder() + .dogId(this.dogId) + .dogName(this.name) + .dogSize(this.dogSize) + .dogProfile(this.profile) + .dogGender(this.gender) + .dogAge(dogAge) + .breed(this.breed) + .weight(this.weight) + .neuteringYn(this.neuteringYn) + .bite(this.bite) + .friendly(this.friendly) + .barking(this.barking) + .rabiesYn(this.rabiesYn) + .adultYn(this.adultYn) + .walkRequest(this.walkRequest) + .walkNote(this.walkNote) + .additionalRequest(this.additionalRequest) + .build(); + } + + @Builder + public Dog(Long memberId, DogProfileReqDto dogProfileReqDto){ + this.name = dogProfileReqDto.getName(); + this.memberId = memberId; + this.dogSize = dogProfileReqDto.getDogSize(); + this.profile = dogProfileReqDto.getProfile(); + this.gender = dogProfileReqDto.getGender(); + this.birthYear = dogProfileReqDto.getBirthYear(); + this.breed = dogProfileReqDto.getBreed(); + this.weight = dogProfileReqDto.getWeight(); + this.neuteringYn = dogProfileReqDto.getNeuteringYn(); + this.bite = dogProfileReqDto.getBite(); + this.friendly = dogProfileReqDto.getFriendly(); + this.barking = dogProfileReqDto.getBarking(); + this.rabiesYn = dogProfileReqDto.getRabiesYn(); + this.adultYn = dogProfileReqDto.getAdultYn(); + this.walkRequest = dogProfileReqDto.getWalkRequest(); + this.walkNote = dogProfileReqDto.getWalkNote(); + this.additionalRequest = dogProfileReqDto.getAdditionalRequest(); + } + + public void updateProfile(DogProfileReqDto dogProfileDto) { + this.name = dogProfileDto.getName(); + this.profile = dogProfileDto.getProfile(); + this.gender = dogProfileDto.getGender(); + this.birthYear = dogProfileDto.getBirthYear(); + this.weight = dogProfileDto.getWeight(); + this.breed = dogProfileDto.getBreed(); + this.dogSize = dogProfileDto.getDogSize(); + this.neuteringYn = dogProfileDto.getNeuteringYn(); + this.bite = dogProfileDto.getBite(); + this.friendly = dogProfileDto.getFriendly(); + this.barking = dogProfileDto.getBarking(); + this.rabiesYn = dogProfileDto.getRabiesYn(); + this.adultYn = dogProfileDto.getAdultYn(); + this.walkRequest = dogProfileDto.getWalkRequest(); + this.walkNote = dogProfileDto.getWalkNote(); + this.additionalRequest = dogProfileDto.getAdditionalRequest(); + } } diff --git a/src/main/java/org/jullaene/walkmong_back/api/dog/dto/req/DogProfileReqDto.java b/src/main/java/org/jullaene/walkmong_back/api/dog/dto/req/DogProfileReqDto.java new file mode 100644 index 0000000..107456b --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/dog/dto/req/DogProfileReqDto.java @@ -0,0 +1,26 @@ +package org.jullaene.walkmong_back.api.dog.dto.req; + +import lombok.Getter; +import org.jullaene.walkmong_back.api.dog.domain.enums.DogSize; +import org.jullaene.walkmong_back.common.enums.Gender; + +@Getter +public class DogProfileReqDto { + private Long memberId; + private String name; + private DogSize dogSize; + private String profile; + private Gender gender; + private Integer birthYear; + private String breed; + private Double weight; + private String neuteringYn; + private String bite; + private String friendly; + private String barking; + private String rabiesYn; + private String adultYn; + private String walkRequest; + private String walkNote; + private String additionalRequest; +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/dog/dto/res/DogProfileResponseDto.java b/src/main/java/org/jullaene/walkmong_back/api/dog/dto/res/DogProfileResponseDto.java new file mode 100644 index 0000000..28b940e --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/dog/dto/res/DogProfileResponseDto.java @@ -0,0 +1,28 @@ +package org.jullaene.walkmong_back.api.dog.dto.res; + +import lombok.Builder; +import lombok.Getter; +import org.jullaene.walkmong_back.api.dog.domain.enums.DogSize; +import org.jullaene.walkmong_back.common.enums.Gender; + +@Getter +@Builder +public class DogProfileResponseDto { + private Long dogId; + private String dogName; + private DogSize dogSize; + private String dogProfile; + private Gender dogGender; + private Integer dogAge; + private String breed; + private Double weight; + private String neuteringYn; + private String bite; + private String friendly; + private String barking; + private String rabiesYn; + private String adultYn; + private String walkRequest; + private String walkNote; + private String additionalRequest; +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/dog/repository/DogRepository.java b/src/main/java/org/jullaene/walkmong_back/api/dog/repository/DogRepository.java index 04e7fb4..657fc26 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/dog/repository/DogRepository.java +++ b/src/main/java/org/jullaene/walkmong_back/api/dog/repository/DogRepository.java @@ -1,9 +1,16 @@ package org.jullaene.walkmong_back.api.dog.repository; +import java.util.List; +import java.util.Optional; import org.jullaene.walkmong_back.api.dog.domain.Dog; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface DogRepository extends JpaRepository { + Optional findByDogIdAndDelYn(Long id, String delYn); + + Boolean existsByNameAndDelYn(String name, String delYn); + + List findByMemberIdAndDelYn(Long memberId, String delYn); } diff --git a/src/main/java/org/jullaene/walkmong_back/api/dog/rest/DogController.java b/src/main/java/org/jullaene/walkmong_back/api/dog/rest/DogController.java index 4d11272..99683e2 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/dog/rest/DogController.java +++ b/src/main/java/org/jullaene/walkmong_back/api/dog/rest/DogController.java @@ -1,13 +1,43 @@ package org.jullaene.walkmong_back.api.dog.rest; +import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.dog.dto.req.DogProfileReqDto; +import org.jullaene.walkmong_back.api.dog.dto.res.DogProfileResponseDto; import org.jullaene.walkmong_back.api.dog.service.DogService; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.jullaene.walkmong_back.common.BasicResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/dog") public class DogController { private final DogService dogService; + + @GetMapping("/profile/{dogId}") + public ResponseEntity> getDogProfile(@PathVariable Long dogId) { + DogProfileResponseDto dogProfile = dogService.getDogProfile(dogId); + return ResponseEntity.ok(BasicResponse.ofSuccess(dogProfile)); + } + + @PostMapping("/register") + public ResponseEntity> registerDogProfile(@Valid @RequestBody DogProfileReqDto dogProfileReqDto) { + return ResponseEntity.ok(BasicResponse.ofSuccess(dogService.registerDogProfile(dogProfileReqDto))); + } + + @PatchMapping("/profile/update/{dogId}") + public ResponseEntity> updateDogProfile( + @PathVariable Long dogId, + @RequestBody DogProfileReqDto dogProfileReqDto) { + DogProfileResponseDto updatedProfile = dogService.updateDogProfile(dogId, dogProfileReqDto); + return ResponseEntity.ok(BasicResponse.ofSuccess(updatedProfile)); + } + + @GetMapping("/list") + public ResponseEntity>> getDogProfiles() { + List dogProfileList = dogService.getDogProfileList(); + return ResponseEntity.ok(BasicResponse.ofSuccess(dogProfileList)); + } } diff --git a/src/main/java/org/jullaene/walkmong_back/api/dog/service/DogService.java b/src/main/java/org/jullaene/walkmong_back/api/dog/service/DogService.java index ad6078e..49593ea 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/dog/service/DogService.java +++ b/src/main/java/org/jullaene/walkmong_back/api/dog/service/DogService.java @@ -1,11 +1,77 @@ package org.jullaene.walkmong_back.api.dog.service; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.dog.domain.Dog; +import org.jullaene.walkmong_back.api.dog.dto.req.DogProfileReqDto; +import org.jullaene.walkmong_back.api.dog.dto.res.DogProfileResponseDto; import org.jullaene.walkmong_back.api.dog.repository.DogRepository; +import org.jullaene.walkmong_back.api.member.domain.Member; +import org.jullaene.walkmong_back.api.member.service.MemberService; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.exception.ErrorType; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class DogService { private final DogRepository dogRepository; + private final MemberService memberService; + + public DogProfileResponseDto getDogProfile(Long dogId) { + Dog dog = dogRepository.findByDogIdAndDelYn(dogId, "N") + .orElseThrow(() -> new CustomException(HttpStatus.NOT_FOUND, ErrorType.DOG_NOT_FOUND)); + + return dog.toDogProfileResponseDto(); + } + + public Long registerDogProfile(DogProfileReqDto dogProfileReqDto) { + + Member member = memberService.getMemberFromUserDetail(); + + //이미 등록된 강아지 프로필이라면 예외처리 + if (dogRepository.existsByNameAndDelYn(dogProfileReqDto.getName(),"N")){ + throw new CustomException(HttpStatus.FORBIDDEN,ErrorType.CANNOT_DUPLICATED_DOG_PROFILE); + } + + Dog dog=Dog.builder() + .memberId(member.getMemberId()) + .dogProfileReqDto(dogProfileReqDto) + .build(); + + return dogRepository.save(dog).getDogId(); + } + + public DogProfileResponseDto updateDogProfile(Long dogId, DogProfileReqDto dogProfileReqDto) { + + Member member = memberService.getMemberFromUserDetail(); + + List dogList = dogRepository.findByMemberIdAndDelYn(member.getMemberId(), "N"); + + Dog dog = dogRepository.findById(dogId) + .orElseThrow(() -> new CustomException(HttpStatus.NOT_FOUND, ErrorType.DOG_NOT_FOUND)); + + boolean isDogOwnedByMember = dogList.stream() + .anyMatch(memberDog -> memberDog.getDogId().equals(dogId)); + + if (!isDogOwnedByMember) { + throw new CustomException(HttpStatus.FORBIDDEN, ErrorType.ACCESS_DENIED); + } + + dog.updateProfile(dogProfileReqDto); + Dog updatedDog = dogRepository.save(dog); + + return updatedDog.toDogProfileResponseDto(); + } + + public List getDogProfileList() { + Member member = memberService.getMemberFromUserDetail(); + List dogs = dogRepository.findByMemberIdAndDelYn(member.getMemberId(), "N"); + + return dogs.stream() + .map(Dog::toDogProfileResponseDto) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/domain/Member.java b/src/main/java/org/jullaene/walkmong_back/api/member/domain/Member.java index 4456dbe..1ddce23 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/member/domain/Member.java +++ b/src/main/java/org/jullaene/walkmong_back/api/member/domain/Member.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.Comment; import org.hibernate.annotations.DynamicUpdate; import org.jullaene.walkmong_back.api.member.domain.enums.Role; +import org.jullaene.walkmong_back.api.member.dto.req.WalkExperienceReq; import org.jullaene.walkmong_back.common.BaseEntity; import org.jullaene.walkmong_back.common.enums.Gender; @@ -80,4 +81,11 @@ public Member (Long memberId, String email, String password, String nickname, this.dogWalkingExperienceYn = dogWalkingExperienceYn; this.availabilityWithSize = availabilityWithSize; } + + public void addWalkingExperience(WalkExperienceReq walkExperienceReq) { + this.dogOwnership=walkExperienceReq.getDogOwnershipYn(); + this.dogWalkingExperienceYn=walkExperienceReq.getDogWalkingExperienceYn(); + this.availabilityWithSize=walkExperienceReq.getAvailabilityWithSize(); + this.introduce=walkExperienceReq.getIntroduction(); + } } diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/dto/req/WalkExperienceReq.java b/src/main/java/org/jullaene/walkmong_back/api/member/dto/req/WalkExperienceReq.java new file mode 100644 index 0000000..a900583 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/member/dto/req/WalkExperienceReq.java @@ -0,0 +1,12 @@ +package org.jullaene.walkmong_back.api.member.dto.req; + +import lombok.Getter; + + +@Getter +public class WalkExperienceReq { + private String dogOwnershipYn; + private String dogWalkingExperienceYn; + private String availabilityWithSize; + private String introduction; +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/repository/MemberRepository.java b/src/main/java/org/jullaene/walkmong_back/api/member/repository/MemberRepository.java index a40196b..52b1f31 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/member/repository/MemberRepository.java +++ b/src/main/java/org/jullaene/walkmong_back/api/member/repository/MemberRepository.java @@ -10,4 +10,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); Boolean existsByEmail(String email); + + Boolean existsByNickname(String nickname); } diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/rest/AuthController.java b/src/main/java/org/jullaene/walkmong_back/api/member/rest/AuthController.java index 22a7a1c..4dc4128 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/member/rest/AuthController.java +++ b/src/main/java/org/jullaene/walkmong_back/api/member/rest/AuthController.java @@ -8,11 +8,11 @@ import org.jullaene.walkmong_back.api.member.dto.req.MemberCreateReq; import org.jullaene.walkmong_back.api.member.service.AuthService; import org.jullaene.walkmong_back.common.BasicResponse; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.exception.ErrorType; +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.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "Auth", description = "인증 관련 api 입니다.") @RestController @@ -33,4 +33,37 @@ public ResponseEntity> login(@Valid @RequestBody LoginReq return ResponseEntity.ok(BasicResponse.ofSuccess(authService.login(loginReq))); } + @Operation(summary = "이메일 중복 확인", description = "이메일 중복 확인") + @PostMapping("/email/duplicate") + public ResponseEntity> emailDuplicate (@RequestParam(name = "email") String email) { + return ResponseEntity.ok(BasicResponse.ofSuccess(authService.duplicateEmail(email))); + } + + @Operation(summary = "닉네임 중복 확인", description = "닉네임 중복 확인") + @PostMapping("/nickname/duplicate") + public ResponseEntity> nicknameDuplicate (@RequestParam(name = "nickname") String nickname) { + return ResponseEntity.ok(BasicResponse.ofSuccess(authService.duplicateNickname(nickname))); + } + + @Operation(summary = "이메일 인증 요청", description = "이메일 인증 요청") + @PostMapping("/email/code/request") + public ResponseEntity> requestVerification(@RequestParam(name = "email") String email) { + try { + return ResponseEntity.ok(BasicResponse.ofSuccess(authService.requestEmailVerification(email))); + } catch (Exception e) { + throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, ErrorType.INTERNAL_SERVER); + } + } + + @PostMapping("/email/code/verify") + public ResponseEntity> verifyCode(@RequestParam(name = "email") String email, + @RequestParam(name = "code") String code) { + boolean isVerified = authService.verifyCode(email, code); + + if (isVerified) { + return ResponseEntity.ok(BasicResponse.ofSuccess("인증에 성공했습니다.")); + } else { + throw new CustomException(HttpStatus.BAD_REQUEST, ErrorType.INVALID_VERIFICATION_CODE); + } + } } \ No newline at end of file diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/rest/MemberController.java b/src/main/java/org/jullaene/walkmong_back/api/member/rest/MemberController.java index f0a3538..3086ea9 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/member/rest/MemberController.java +++ b/src/main/java/org/jullaene/walkmong_back/api/member/rest/MemberController.java @@ -1,10 +1,15 @@ package org.jullaene.walkmong_back.api.member.rest; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.member.domain.Member; +import org.jullaene.walkmong_back.api.member.dto.req.WalkExperienceReq; import org.jullaene.walkmong_back.api.member.service.MemberService; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.jullaene.walkmong_back.common.BasicResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @Tag(name = "Member", description = "멤버 정보 관련 api 입니다.") @RestController @@ -12,4 +17,10 @@ @RequestMapping("/api/v1/member") public class MemberController { private final MemberService memberService; + + @Operation(summary = "산책추가정보등록", description = "산책추가정보등록") + @PostMapping("/experience") + public ResponseEntity> registerWalkExperienceInfo(@Valid @RequestBody WalkExperienceReq walkExperienceReq) { + return ResponseEntity.ok(BasicResponse.ofSuccess(memberService.registerWalkingExperience(walkExperienceReq))); + } } diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/service/AuthService.java b/src/main/java/org/jullaene/walkmong_back/api/member/service/AuthService.java index b6daed1..f3a093e 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/member/service/AuthService.java +++ b/src/main/java/org/jullaene/walkmong_back/api/member/service/AuthService.java @@ -8,17 +8,29 @@ import org.jullaene.walkmong_back.common.exception.CustomException; import org.jullaene.walkmong_back.common.exception.ErrorType; import org.jullaene.walkmong_back.common.utils.JwtTokenUtil; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + @RequiredArgsConstructor @Service public class AuthService { private final MemberRepository memberRepository; private final JwtTokenUtil jwtTokenUtil; private final PasswordEncoder passwordEncoder; + private final EmailService emailService; /** * 로그인 @@ -37,11 +49,6 @@ public String login(LoginReq loginReq) { */ @Transactional public Long createAccount(MemberCreateReq memberCreateReq) { - - if (memberRepository.existsByEmail(memberCreateReq.getEmail())) { - throw new CustomException(HttpStatus.CONFLICT, ErrorType.ALREADY_EXIST_USER); - } - Member member = Member.builder() .email(memberCreateReq.getEmail()) .nickname(memberCreateReq.getNickname()) @@ -51,6 +58,43 @@ public Long createAccount(MemberCreateReq memberCreateReq) { return memberRepository.save(member).getMemberId(); } + /** + * 이메일 중복 확인 + * */ + @Transactional(readOnly = true) + public String duplicateEmail (String email) { + if (memberRepository.existsByEmail(email)) { + throw new CustomException(HttpStatus.CONFLICT, ErrorType.ALREADY_EXIST_USER); + } + return "사용 가능한 이메일입니다."; + } + + /** + * 닉네임 중복 확인 + * */ + @Transactional(readOnly = true) + public String duplicateNickname(String nickname) { + if (memberRepository.existsByNickname(nickname)) { + throw new CustomException(HttpStatus.CONFLICT, ErrorType.ALREADY_EXIST_NICKNAME); + } + return "사용 가능한 닉네임입니다."; + } + + /** + * 이메일 인증 번호 요청 + * */ + public String requestEmailVerification(String email) { + emailService.sendVerificationCode(email); + return "인증 번호 전송 완료"; + } + + /** + * 이메일 인증 번호 확인 + * */ + public boolean verifyCode(String email, String code) { + return emailService.verifyCode(email, code); + } + /** * 이메일을 이용하여 Account 정보를 찾는 API */ diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/service/EmailService.java b/src/main/java/org/jullaene/walkmong_back/api/member/service/EmailService.java new file mode 100644 index 0000000..6335eab --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/member/service/EmailService.java @@ -0,0 +1,87 @@ +package org.jullaene.walkmong_back.api.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.time.Duration; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class EmailService { + private final RedisTemplate redisTemplate; + private final JavaMailSender mailSender; + + private static final int CODE_EXPIRATION_MINUTES = 10; + private static final int CODE_LENGTH = 6; + @Value("${smtp.mail.username}") + private String managerName; + + /** + * 인증번호 생성 및 redis에 저장 + * */ + public void sendVerificationCode(String email) { + String redisKey = "email_verification:" + email; + + // 기존 키가 존재하면 삭제 + if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) { + redisTemplate.delete(redisKey); + } + + // 새 인증번호 생성 및 처리 + String verificationCode = generateVerificationCode(); + redisTemplate.opsForValue().set( + redisKey, + verificationCode, + Duration.ofMinutes(CODE_EXPIRATION_MINUTES) + ); + + // 이메일 발송 + sendVerificationEmail(email, verificationCode); + } + + /** + * 인증번호 검증 + * */ + public boolean verifyCode(String email, String userInputCode) { + String redisKey = "email_verification:" + email; + String storedCode = redisTemplate.opsForValue().get(redisKey); + + // 코드 검증 + if (storedCode != null && storedCode.equals(userInputCode)) { + // 인증 성공 시 Redis에서 삭제 + redisTemplate.delete(redisKey); + return true; + } + return false; + } + + /** + * 인증번호 생성 + * */ + private String generateVerificationCode() { + SecureRandom random = new SecureRandom(); + return random.ints(CODE_LENGTH, 0, 10) + .mapToObj(String::valueOf) + .collect(Collectors.joining()); + } + + /** + * 이메일로 인증번호 전송 + * */ + private void sendVerificationEmail(String to, String verificationCode) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(managerName); + message.setTo(to); + message.setSubject("[WAlKMONG] 이메일 인증 코드"); + message.setText("인증 코드는 [" + verificationCode + "] 입니다. " + + CODE_EXPIRATION_MINUTES + "분 이내에 입력해주세요."); + + mailSender.send(message); + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/member/service/MemberService.java b/src/main/java/org/jullaene/walkmong_back/api/member/service/MemberService.java index bf50eab..4fd9184 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/member/service/MemberService.java +++ b/src/main/java/org/jullaene/walkmong_back/api/member/service/MemberService.java @@ -1,11 +1,39 @@ package org.jullaene.walkmong_back.api.member.service; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.member.domain.Member; +import org.jullaene.walkmong_back.api.member.dto.req.WalkExperienceReq; import org.jullaene.walkmong_back.api.member.repository.MemberRepository; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.user.CustomUserDetail; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import static org.jullaene.walkmong_back.common.exception.ErrorType.USER_NOT_AUTHENTICATED; + @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; + + /** + * CustomUserDetail에서 member 가져오기 + * */ + public Member getMemberFromUserDetail () { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetail customUserDetail) { + return customUserDetail.getMember(); // member 객체 가져오기 + } + throw new CustomException(HttpStatus.UNAUTHORIZED, USER_NOT_AUTHENTICATED); + } + + public Long registerWalkingExperience(@Valid WalkExperienceReq walkExperienceReq) { + getMemberFromUserDetail().addWalkingExperience(walkExperienceReq); + + return getMemberFromUserDetail().getMemberId(); + } } diff --git a/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java b/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java index 3a81491..d9d6fb0 100644 --- a/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java +++ b/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java @@ -18,6 +18,8 @@ public enum ErrorType { INVALID_TOKEN("유효하지 않은 토큰입니다."), INTERNAL_SERVER("서버 오류입니다."), UNAUTHORIZED_UPDATE("수정 권한이 없는 유저입니다."), + DOG_NOT_FOUND("존재하지 않는 강아지입니다."), + CANNOT_DUPLICATED_DOG_PROFILE("이미 등록된 강아지입니다") ; private String message; diff --git a/src/main/java/org/jullaene/walkmong_back/config/SMTPConfig.java b/src/main/java/org/jullaene/walkmong_back/config/SMTPConfig.java new file mode 100644 index 0000000..17a0a67 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/config/SMTPConfig.java @@ -0,0 +1,42 @@ +package org.jullaene.walkmong_back.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 java.util.Properties; + +@Configuration +public class SMTPConfig { + + @Value("${smtp.mail.username}") + private String username; + @Value("${smtp.mail.password}") + private String password; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); + + javaMailSender.setHost("smtp.gmail.com"); + javaMailSender.setUsername(username); + javaMailSender.setPassword(password); + + javaMailSender.setPort(587); + + javaMailSender.setJavaMailProperties(getMailProperties()); + + return javaMailSender; + } + + private Properties getMailProperties() { + Properties properties = new Properties(); + properties.setProperty("mail.transport.protocol", "smtp"); + properties.setProperty("mail.smtp.auth", "true"); + properties.setProperty("mail.smtp.starttls.enable", "true"); + + return properties; + } +} \ No newline at end of file