diff --git a/src/main/generated/ceos/phototoground/domain/photographer/entity/QPhotographer.java b/src/main/generated/ceos/phototoground/domain/photographer/entity/QPhotographer.java index ad86aff..d995fc5 100644 --- a/src/main/generated/ceos/phototoground/domain/photographer/entity/QPhotographer.java +++ b/src/main/generated/ceos/phototoground/domain/photographer/entity/QPhotographer.java @@ -35,6 +35,8 @@ public class QPhotographer extends EntityPathBase { public final NumberPath id = createNumber("id", Long.class); + public final BooleanPath isFirst = createBoolean("isFirst"); + public final EnumPath myUniv = createEnum("myUniv", MyUniv.class); public final StringPath password = createString("password"); diff --git a/src/main/java/ceos/phototoground/domain/photographer/controller/PhotographerController.java b/src/main/java/ceos/phototoground/domain/photographer/controller/PhotographerController.java index 7240a47..6439e52 100644 --- a/src/main/java/ceos/phototoground/domain/photographer/controller/PhotographerController.java +++ b/src/main/java/ceos/phototoground/domain/photographer/controller/PhotographerController.java @@ -2,6 +2,7 @@ import ceos.phototoground.domain.customer.dto.CustomUserDetails; +import ceos.phototoground.domain.customer.dto.PasswordUpdateDto; import ceos.phototoground.domain.photographer.dto.PhotographerBottomDTO; import ceos.phototoground.domain.photographer.dto.PhotographerIdDTO; import ceos.phototoground.domain.photographer.dto.PhotographerIntroDTO; @@ -9,12 +10,16 @@ import ceos.phototoground.domain.photographer.dto.PhotographerResponseDTO; import ceos.phototoground.domain.photographer.dto.PhotographerSearchListDTO; import ceos.phototoground.domain.photographer.service.PhotographerService; +import ceos.phototoground.global.dto.SuccessResponseDto; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -26,6 +31,17 @@ public class PhotographerController { private final PhotographerService photographerService; + // 작가 비밀번호 수정 + @PatchMapping("/password") + public ResponseEntity> updatePassword(@AuthenticationPrincipal CustomUserDetails userDetails, + @Valid @RequestBody PasswordUpdateDto passwordUpdateDto) { + Long photographerId = userDetails.getPhotographer().getId(); + + photographerService.updatePassword(photographerId, passwordUpdateDto); + + return ResponseEntity.ok(SuccessResponseDto.successMessage("비밀번호가 성공적으로 변경되었습니다.")); + } + // 작가 리스트 조회 (필터링 포함) @GetMapping public ResponseEntity getPhotographerList( diff --git a/src/main/java/ceos/phototoground/domain/photographer/entity/Photographer.java b/src/main/java/ceos/phototoground/domain/photographer/entity/Photographer.java index 5075704..fd47cea 100644 --- a/src/main/java/ceos/phototoground/domain/photographer/entity/Photographer.java +++ b/src/main/java/ceos/phototoground/domain/photographer/entity/Photographer.java @@ -47,6 +47,10 @@ public class Photographer extends BaseTimeEntity { @NotNull private int bornYear; + // 첫 로그인 여부 확인 + @Builder.Default + private boolean isFirst = true; + @NotNull @Enumerated(EnumType.STRING) private MyUniv myUniv; //작가의 대학교 @@ -58,4 +62,16 @@ public class Photographer extends BaseTimeEntity { @OneToOne(mappedBy = "photographer", cascade = CascadeType.ALL, orphanRemoval = true) private PhotoProfile photoProfile; //양방향 연관관계 설정 + // 첫 로그인 상태를 false로 변경하는 메서드 + public void markAsLoggedIn() { + if (this.isFirst) { + this.isFirst = false; + } + } + + // 비밀번호 수정 메서드 + public void updatePassword(String password) { + this.password = password; + System.out.println("업데이트 완료"); + } } diff --git a/src/main/java/ceos/phototoground/domain/photographer/service/PhotographerService.java b/src/main/java/ceos/phototoground/domain/photographer/service/PhotographerService.java index 6a098c8..21f9adc 100644 --- a/src/main/java/ceos/phototoground/domain/photographer/service/PhotographerService.java +++ b/src/main/java/ceos/phototoground/domain/photographer/service/PhotographerService.java @@ -1,6 +1,8 @@ package ceos.phototoground.domain.photographer.service; import ceos.phototoground.domain.customer.dto.CustomUserDetails; +import ceos.phototoground.domain.customer.dto.PasswordUpdateDto; +import ceos.phototoground.domain.customer.entity.Customer; import ceos.phototoground.domain.customer.entity.UserRole; import ceos.phototoground.domain.follow.entity.Follow; import ceos.phototoground.domain.follow.service.FollowService; @@ -30,6 +32,7 @@ import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,14 +47,31 @@ public class PhotographerService { private final PhotoStyleService photoStyleService; private final PostService postService; private final FollowService followService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; - @Transactional public Photographer findPhotographerById(Long photographerId) { return photographerRepository.findById(photographerId) .orElseThrow(() -> new CustomException(ErrorCode.PHOTOGRAPHER_NOT_FOUND)); } + // 작가 비밀번호 수정 + @Transactional + public void updatePassword(Long photographerId, PasswordUpdateDto passwordUpdateDto) { + // 고객 엔티티 조회 + Photographer photographer = photographerRepository.findById(photographerId) + .orElseThrow(() -> new CustomException(ErrorCode.PHOTOGRAPHER_NOT_FOUND)); + + // 새 비밀번호 검증 + validateNewPassword(passwordUpdateDto.getPassword(), photographer.getPassword()); + + // 새 비밀번호 암호화 후 저장 + String encryptedPassword = bCryptPasswordEncoder.encode(passwordUpdateDto.getPassword()); + photographer.updatePassword(encryptedPassword); + + // 작가 정보 저장 + photographerRepository.save(photographer); + } public PhotographerListDTO getPhotographerList(Long cursor, int size, String univ, String gender) { @@ -206,6 +226,33 @@ public Photographer findById(Long photographerId) { .orElseThrow(() -> new CustomException(ErrorCode.PHOTOGRAPHER_NOT_FOUND)); } + // 비밀번호 유효성 검증 + private void validateNewPassword(String newPassword, String existingPassword) { + if (newPassword == null || newPassword.length() < 8 || newPassword.length() > 12) { + throw new CustomException(ErrorCode.INVALID_PASSWORD, "새 비밀번호는 8자 이상, 12자 이하로 설정해야 합니다."); + } + + StringBuilder errorMessages = new StringBuilder(); + + if (!newPassword.matches(".*[a-zA-Z].*")) { + errorMessages.append("영문자가 포함되어야 합니다. "); + } + if (!newPassword.matches(".*\\d.*")) { + errorMessages.append("숫자가 포함되어야 합니다. "); + } + if (!newPassword.matches(".*[\\$!@%&\\*].*")) { + errorMessages.append("특수문자가 포함되어야 합니다."); + } + + if (errorMessages.length() > 0) { + throw new CustomException(ErrorCode.INVALID_PASSWORD, errorMessages.toString().trim()); + } + + if (bCryptPasswordEncoder.matches(newPassword, existingPassword)) { + throw new CustomException(ErrorCode.REUSED_PASSWORD); + } + } + public PhotographerIdDTO getMyId(CustomUserDetails customUserDetails) { Long myId = customUserDetails.getPhotographer().getId(); return PhotographerIdDTO.from(myId); diff --git a/src/main/java/ceos/phototoground/global/config/SecurityConfig.java b/src/main/java/ceos/phototoground/global/config/SecurityConfig.java index d190fe1..6386d6c 100644 --- a/src/main/java/ceos/phototoground/global/config/SecurityConfig.java +++ b/src/main/java/ceos/phototoground/global/config/SecurityConfig.java @@ -81,7 +81,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 로그인 필터 설정 (/login) http.addFilterAt( - new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository), + new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository, photographerRepository), UsernamePasswordAuthenticationFilter.class); // 로그아웃 필터 설정 (/logout) diff --git a/src/main/java/ceos/phototoground/global/jwt/LoginFilter.java b/src/main/java/ceos/phototoground/global/jwt/LoginFilter.java index c8d3da7..83b18cb 100644 --- a/src/main/java/ceos/phototoground/global/jwt/LoginFilter.java +++ b/src/main/java/ceos/phototoground/global/jwt/LoginFilter.java @@ -1,10 +1,12 @@ package ceos.phototoground.global.jwt; import ceos.phototoground.domain.customer.dto.LoginRequestDto; +import ceos.phototoground.domain.photographer.repository.PhotographerRepository; import ceos.phototoground.global.dto.ErrorResponseDto; import ceos.phototoground.global.dto.SuccessResponseDto; import ceos.phototoground.global.entity.RefreshEntity; import ceos.phototoground.global.entity.RefreshRepository; +import ceos.phototoground.global.util.SpringContext; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.http.Cookie; @@ -30,6 +32,7 @@ public class LoginFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; private final JWTUtil jwtUtil; private final RefreshRepository refreshRepository; + private final PhotographerRepository photographerRepository; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) @@ -88,6 +91,22 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR userData.put("username", username); userData.put("role", role); // 단일 권한 추가 + // 첫 로그인 여부 확인 및 응답 추가 (Photographer만 해당) + if (role.equals("ROLE_PHOTOGRAPHER")) { // Photographer 역할인 경우 + PhotographerRepository photographerRepository = // Repository 주입 + SpringContext.getBean(PhotographerRepository.class); + + photographerRepository.findByEmail(username).ifPresent(photographer -> { + userData.put("isFirst", photographer.isFirst()); + + // 첫 로그인이라면 isFirst 업데이트 + if (photographer.isFirst()) { + photographer.markAsLoggedIn(); // 첫 로그인 이후 false로 변경 + photographerRepository.save(photographer); + } + }); + } + // 성공 응답 생성 SuccessResponseDto> successResponse = SuccessResponseDto.success( 200, diff --git a/src/main/java/ceos/phototoground/global/util/SpringContext.java b/src/main/java/ceos/phototoground/global/util/SpringContext.java new file mode 100644 index 0000000..5aef02c --- /dev/null +++ b/src/main/java/ceos/phototoground/global/util/SpringContext.java @@ -0,0 +1,16 @@ +package ceos.phototoground.global.util; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class SpringContext { + private static ApplicationContext context; + + public SpringContext(ApplicationContext applicationContext) { + SpringContext.context = applicationContext; + } + + public static T getBean(Class beanClass) { + return context.getBean(beanClass); + } +} \ No newline at end of file