diff --git a/docker-compose.yml b/docker-compose.yml index 58fd7a2..8497d36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,9 +54,6 @@ - app-network - volumes: - redis-data: - networks: app-network: - name: app-network \ No newline at end of file + name: hyetaekon-network \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/controller/UserInterestController.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/controller/UserInterestController.java index c9e47d7..6e15c52 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/controller/UserInterestController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/controller/UserInterestController.java @@ -1,7 +1,8 @@ package com.hyetaekon.hyetaekon.UserInterest.controller; -import com.hyetaekon.hyetaekon.UserInterest.dto.UserInterestRequestDto; -import com.hyetaekon.hyetaekon.UserInterest.dto.UserInterestResponseDto; +import com.hyetaekon.hyetaekon.UserInterest.dto.CategorizedInterestsResponseDto; +import com.hyetaekon.hyetaekon.UserInterest.dto.CategorizedInterestsWithSelectionDto; +import com.hyetaekon.hyetaekon.UserInterest.dto.InterestSelectionRequestDto; import com.hyetaekon.hyetaekon.UserInterest.entity.UserInterestEnum; import com.hyetaekon.hyetaekon.UserInterest.service.UserInterestService; import com.hyetaekon.hyetaekon.common.jwt.CustomUserDetails; @@ -13,6 +14,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @RestController @@ -24,31 +26,32 @@ public class UserInterestController { // 선택할 키워드 목록 조회 @GetMapping - public ResponseEntity getAvailableInterests() { - // Enum에서 displayName 값 추출 - List Interests = Arrays.stream(UserInterestEnum.values()) - .map(UserInterestEnum::getDisplayName) - .collect(Collectors.toList()); - return ResponseEntity.ok(new UserInterestResponseDto(Interests)); + public ResponseEntity getAvailableInterests() { + // Enum에서 카테고리별 displayName 값 추출 + Map> categorizedInterests = Arrays.stream(UserInterestEnum.values()) + .collect(Collectors.groupingBy( + UserInterestEnum::getCategory, + Collectors.mapping(UserInterestEnum::getDisplayName, Collectors.toList()) + )); + return ResponseEntity.ok(new CategorizedInterestsResponseDto(categorizedInterests)); } - - // 개인 키워드 목록 조회 + // 모든 관심사와 사용자 선택 여부 조회 @GetMapping("/me") - public ResponseEntity getMyInterest(@AuthenticationPrincipal CustomUserDetails userDetails) { + public ResponseEntity getMyInterestsWithSelection( + @AuthenticationPrincipal CustomUserDetails userDetails) { Long userId = userDetails.getId(); - return ResponseEntity.ok(userInterestService.getUserInterestsByUserId(userId)); + return ResponseEntity.ok(userInterestService.getUserInterestsWithSelection(userId)); } - // 선택한 키워드 목록 저장 + // 선택한 관심사 저장 @PostMapping("/me") - public ResponseEntity replaceInterests( + public ResponseEntity saveInterests( @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody UserInterestRequestDto UserInterestRequest + @RequestBody InterestSelectionRequestDto requestDto ) { Long userId = userDetails.getId(); - List Interests = UserInterestRequest.getInterests(); - userInterestService.replaceUserInterests(userId, Interests); + userInterestService.saveUserInterests(userId, requestDto.getAllInterests()); return ResponseEntity.status(HttpStatus.OK).build(); } } diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/CategorizedInterestsResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/CategorizedInterestsResponseDto.java new file mode 100644 index 0000000..f156cbc --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/CategorizedInterestsResponseDto.java @@ -0,0 +1,14 @@ +package com.hyetaekon.hyetaekon.UserInterest.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.Map; + +@Getter +@AllArgsConstructor +public class CategorizedInterestsResponseDto { + private Map> categorizedInterests; +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/UserInterestResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/CategorizedInterestsWithSelectionDto.java similarity index 53% rename from src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/UserInterestResponseDto.java rename to src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/CategorizedInterestsWithSelectionDto.java index 53514ac..907417a 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/UserInterestResponseDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/CategorizedInterestsWithSelectionDto.java @@ -4,10 +4,10 @@ import lombok.Getter; import java.util.List; - +import java.util.Map; @Getter @AllArgsConstructor -public class UserInterestResponseDto { - private List interests; +public class CategorizedInterestsWithSelectionDto { + private Map> categorizedInterests; } diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/InterestItemDto.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/InterestItemDto.java new file mode 100644 index 0000000..f572bda --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/InterestItemDto.java @@ -0,0 +1,11 @@ +package com.hyetaekon.hyetaekon.UserInterest.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class InterestItemDto { + private String name; // 관심사 이름 + private boolean selected; // 선택 여부 +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/InterestSelectionRequestDto.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/InterestSelectionRequestDto.java new file mode 100644 index 0000000..a092d0e --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/InterestSelectionRequestDto.java @@ -0,0 +1,30 @@ +package com.hyetaekon.hyetaekon.UserInterest.dto; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Getter +public class InterestSelectionRequestDto { + private Map> categorizedInterests; + + // 기존 필드 유지 + private List selectedInterests; + + // categorizedInterests에서 모든 관심사를 추출하는 메소드 + public List getAllInterests() { + if (selectedInterests != null) { + return selectedInterests; + } + + if (categorizedInterests == null) { + return new ArrayList<>(); + } + + List allInterests = new ArrayList<>(); + categorizedInterests.values().forEach(allInterests::addAll); + return allInterests; + } +} \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/UserInterestRequestDto.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/UserInterestRequestDto.java deleted file mode 100644 index 4dac5b7..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/dto/UserInterestRequestDto.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.hyetaekon.hyetaekon.UserInterest.dto; - -import lombok.Getter; - -import java.util.List; - -@Getter -public class UserInterestRequestDto { - private List interests; -} \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java index 115d4b5..92f8296 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java @@ -5,35 +5,36 @@ @Getter public enum UserInterestEnum { // ServiceCategory 관련 관심사 - CHILDCARE_EDUCATION("보육·교육"), - HOUSING_INDEPENDENCE("주거·자립"), - ADMINISTRATION_SAFETY("행정·안전"), - AGRICULTURE_FISHERY("농림축산어업"), - EMPLOYMENT_STARTUP("고용·창업"), - HEALTH_MEDICAL("보건·의료"), - CULTURE_ENVIRONMENT("문화·환경"), - LIFE_STABILITY("생활안정"), + CHILDCARE_EDUCATION("보육·교육", "관심주제"), + HOUSING_INDEPENDENCE("주거·자립", "관심주제"), + ADMINISTRATION_SAFETY("행정·안전", "관심주제"), + AGRICULTURE_FISHERY("농림축산어업", "관심주제"), + EMPLOYMENT_STARTUP("고용·창업", "관심주제"), + HEALTH_MEDICAL("보건·의료", "관심주제"), + CULTURE_ENVIRONMENT("문화·환경", "관심주제"), + LIFE_STABILITY("생활안정", "관심주제"), // SpecialGroup 관련 관심사 - IS_MULTI_CULTURAL("다문화가족"), - IS_NORTH_KOREAN_DEFECTOR("북한이탈주민"), - IS_SINGLE_PARENT_FAMILY("한부모가정/조손가정"), - IS_SINGLE_MEMBER_HOUSEHOLD("1인가구"), - IS_DISABLED("장애인"), - IS_NATIONAL_MERIT_RECIPIENT("국가보훈대상자"), - IS_CHRONIC_ILLNESS("질병/질환자"), + IS_MULTI_CULTURAL("다문화가족", "가구형태"), + IS_NORTH_KOREAN_DEFECTOR("북한이탈주민", "가구형태"), + IS_SINGLE_PARENT_FAMILY("한부모가정/조손가정", "가구형태"), + IS_SINGLE_MEMBER_HOUSEHOLD("1인가구", "가구형태"), + IS_DISABLED("장애인", "가구형태"), + IS_NATIONAL_MERIT_RECIPIENT("국가보훈대상자", "가구형태"), + IS_CHRONIC_ILLNESS("질병/질환자", "가구형태"), // FamilyType 관련 관심사 - // IS_NOT_APPLICABLE("해당사항 없음"), - IS_MULTI_CHILDREN_FAMILY("다자녀가구"), - IS_NON_HOUSING_HOUSEHOLD("무주택세대"), - IS_NEW_RESIDENCE("신규전입"), - IS_EXTENDED_FAMILY("확대가족"); + // IS_NOT_APPLICABLE("해당사항 없음", "가구상황"), + IS_MULTI_CHILDREN_FAMILY("다자녀가구", "가구상황"), + IS_NON_HOUSING_HOUSEHOLD("무주택세대", "가구상황"), + IS_NEW_RESIDENCE("신규전입", "가구상황"), + IS_EXTENDED_FAMILY("확대가족", "가구상황"); private final String displayName; + private final String category; - UserInterestEnum(String displayName) { + UserInterestEnum(String displayName, String category) { this.displayName = displayName; + this.category = category; } - } \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/service/UserInterestService.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/service/UserInterestService.java index df06dde..84d1a91 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/service/UserInterestService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/service/UserInterestService.java @@ -1,7 +1,9 @@ package com.hyetaekon.hyetaekon.UserInterest.service; -import com.hyetaekon.hyetaekon.UserInterest.dto.UserInterestResponseDto; +import com.hyetaekon.hyetaekon.UserInterest.dto.CategorizedInterestsWithSelectionDto; +import com.hyetaekon.hyetaekon.UserInterest.dto.InterestItemDto; import com.hyetaekon.hyetaekon.UserInterest.entity.UserInterest; +import com.hyetaekon.hyetaekon.UserInterest.entity.UserInterestEnum; import com.hyetaekon.hyetaekon.UserInterest.repository.UserInterestRepository; import com.hyetaekon.hyetaekon.common.exception.ErrorCode; import com.hyetaekon.hyetaekon.common.exception.GlobalException; @@ -12,7 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; @Slf4j @Service @@ -21,26 +24,58 @@ public class UserInterestService { private final UserRepository userRepository; private final UserInterestRepository userInterestRepository; - // 나의 관심사 조회 + // 모든 관심사 목록과 사용자 선택 여부 함께 조회 @Transactional(readOnly = true) - public UserInterestResponseDto getUserInterestsByUserId(Long userId) { + public CategorizedInterestsWithSelectionDto getUserInterestsWithSelection(Long userId) { User user = userRepository.findByIdAndDeletedAtIsNull(userId) .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); - List userInterests = user.getInterests().stream() + // 사용자가 선택한 관심사 목록 + List selectedInterests = user.getInterests().stream() .map(UserInterest::getInterest) .toList(); - log.debug("회원 관심사 조회 - 유저 ID: {}, 관심사: {}", userId, userInterests); - return new UserInterestResponseDto(userInterests); + // 카테고리별로 모든 관심사를 포함하되, 선택 여부 표시 + Map> result = new HashMap<>(); + + Arrays.stream(UserInterestEnum.values()) + .forEach(interestEnum -> { + String category = interestEnum.getCategory(); + String displayName = interestEnum.getDisplayName(); + boolean isSelected = selectedInterests.contains(displayName); + + if (!result.containsKey(category)) { + result.put(category, new ArrayList<>()); + } + + result.get(category).add(new InterestItemDto(displayName, isSelected)); + }); + + log.debug("회원 관심사 조회 (선택 여부 포함) - 유저 ID: {}", userId); + return new CategorizedInterestsWithSelectionDto(result); } + // 선택한 관심사 저장 @Transactional - public void replaceUserInterests(Long userId, List Interests) { - // Validate Interest size (optional, assuming max 5) - if (Interests.size() > 5) { - throw new GlobalException( - ErrorCode.INTEREST_LIMIT_EXCEEDED); // Custom error for exceeding limit + public void saveUserInterests(Long userId, List selectedInterests) { + if (selectedInterests == null) { + selectedInterests = new ArrayList<>(); // 빈 리스트로 초기화 + } + + // 최대 선택 개수 검증 + if (selectedInterests.size() > 5) { + throw new GlobalException(ErrorCode.INTEREST_LIMIT_EXCEEDED); + } + + // 유효한 관심사인지 검증 + Set validInterests = Arrays.stream(UserInterestEnum.values()) + .map(UserInterestEnum::getDisplayName) + .collect(Collectors.toSet()); + + for (String interest : selectedInterests) { + if (!validInterests.contains(interest)) { + throw new GlobalException(ErrorCode.INVALID_INTEREST); + } } User user = userRepository.findByIdAndDeletedAtIsNull(userId) @@ -50,14 +85,30 @@ public void replaceUserInterests(Long userId, List Interests) { user.getInterests().clear(); // 새 관심사 추가 - for (String Interest : Interests) { + for (String interest : selectedInterests) { UserInterest newInterest = UserInterest.builder() .user(user) - .interest(Interest) + .interest(interest) .build(); userInterestRepository.save(newInterest); } - log.debug("회원 관심사 갱신 - 유저 ID: {}, 새 관심사 목록: {}", userId, Interests); + log.debug("회원 관심사 갱신 - 유저 ID: {}, 선택 관심사: {}", userId, selectedInterests); } + + /* // 나의 관심사 조회 + @Transactional(readOnly = true) + public UserInterestResponseDto getUserInterestsByUserId(Long userId) { + User user = userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); + + List userInterests = user.getInterests().stream() + .map(UserInterest::getInterest) + .toList(); + + log.debug("회원 관심사 조회 - 유저 ID: {}, 관심사: {}", userId, userInterests); + return new UserInterestResponseDto(userInterests); + } + + */ } diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java b/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java index 0283a1a..2d23372 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java @@ -12,7 +12,8 @@ public class SecurityPath { "/", "/api/services", "/api/services/category/*", - "/api/services/detail/*" + "/api/services/detail/*", + "/api/public-data/serviceList/test" }; @@ -32,7 +33,7 @@ public class SecurityPath { "/api/admin/users/**", "/api/public-data/serviceDetailList", "/api/public-data/supportConditionsList", - "/api/public-data/serviceList/**" + "/api/public-data/serviceList" }; } diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/exception/ErrorCode.java b/src/main/java/com/hyetaekon/hyetaekon/common/exception/ErrorCode.java index 6dc5723..215ae9a 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/exception/ErrorCode.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/exception/ErrorCode.java @@ -30,6 +30,7 @@ public enum ErrorCode { FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN, "AUTH-011", "잘못된 접근입니다."), NOT_SUSPENDED_USER(HttpStatus.BAD_REQUEST, "ACCOUNT-008", "정지 상태가 아닌 회원입니다."), INVALID_SUSPEND_TIME(HttpStatus.BAD_REQUEST, "ACCOUNT-009", "정지 기간이 유효하지 않습니다."), + PASSWORD_CONFIRM_MISMATCH(HttpStatus.BAD_REQUEST, "ACCOUNT-010", "새 비밀번호와 확인 비밀번호가 일치하지 않습니다."), // 북마크 BOOKMARK_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKMARK-001", "북마크한 유저를 찾을 수 없습니다."), @@ -43,6 +44,7 @@ public enum ErrorCode { // 관심사 선택 제한 INTEREST_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "INTEREST-001", "관심사는 최대 6개까지만 등록 가능합니다."), + INVALID_INTEREST(HttpStatus.BAD_REQUEST, "INTEREST-002", "유효하지 않은 관심사입니다."), // 공공서비스 // 유효 JACODE 확인 diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/controller/PublicServiceDataController.java b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/controller/PublicServiceDataController.java index 96ff0e4..8935573 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/controller/PublicServiceDataController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/controller/PublicServiceDataController.java @@ -69,7 +69,7 @@ public ResponseEntity createAndStoreSupportConditionsList() { /** * 페이지 단위 공공서비스 목록 조회 (테스트용) */ - @GetMapping("/serviceList") + @GetMapping("/serviceList/test") public ResponseEntity> getServiceListByPage( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "100") int perPage) { diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/SpecialGroup.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/SpecialGroup.java index 718332b..c720ae8 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/SpecialGroup.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/SpecialGroup.java @@ -9,7 +9,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@Table(name = "spcial_group") +@Table(name = "special_group") public class SpecialGroup { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java b/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java index 84b45f9..6546a5d 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java @@ -3,10 +3,7 @@ import com.hyetaekon.hyetaekon.common.jwt.CustomUserDetails; import com.hyetaekon.hyetaekon.publicservice.dto.PublicServiceListResponseDto; import com.hyetaekon.hyetaekon.publicservice.service.PublicServiceHandler; -import com.hyetaekon.hyetaekon.user.dto.UserResponseDto; -import com.hyetaekon.hyetaekon.user.dto.UserSignUpRequestDto; -import com.hyetaekon.hyetaekon.user.dto.UserSignUpResponseDto; -import com.hyetaekon.hyetaekon.user.dto.UserUpdateRequestDto; +import com.hyetaekon.hyetaekon.user.dto.*; import com.hyetaekon.hyetaekon.user.service.UserService; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; @@ -44,20 +41,28 @@ public ResponseEntity registerUser(@RequestBody @Valid Us public ResponseEntity getMyInfo(@AuthenticationPrincipal CustomUserDetails userDetails) { Long userId = userDetails.getId(); UserResponseDto userInfo = userService.getMyInfo(userId); - return ResponseEntity.ok(userInfo); } // 회원 정보 수정 api - @PutMapping("/users/me") - public ResponseEntity updateMyInfo( + @PutMapping("/users/me/profile") + public ResponseEntity updateMyProfile( @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody @Valid UserUpdateRequestDto userUpdateRequestDto + @RequestBody @Valid UserProfileUpdateDto profileUpdateDto ) { - Long userId = userDetails.getId(); + return ResponseEntity.ok(userService.updateUserProfile(userId, profileUpdateDto)); + } - return ResponseEntity.ok(userService.updateUser(userId, userUpdateRequestDto)); + // 비밀번호 변경 API + @PutMapping("/users/me/password") + public ResponseEntity updateMyPassword( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody @Valid UserPasswordUpdateDto passwordUpdateDto + ) { + Long userId = userDetails.getId(); + userService.updateUserPassword(userId, passwordUpdateDto); + return ResponseEntity.ok().build(); } // 회원 탈퇴 diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserPasswordUpdateDto.java b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserPasswordUpdateDto.java new file mode 100644 index 0000000..c0e91d8 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserPasswordUpdateDto.java @@ -0,0 +1,23 @@ +package com.hyetaekon.hyetaekon.user.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +@Builder +public class UserPasswordUpdateDto { + @NotBlank(message = "현재 비밀번호는 필수입니다.") + private String currentPassword; + + @NotBlank(message = "새 비밀번호는 필수입니다.") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$", + message = "비밀번호는 8자 이상 20자 이하여야 하며, 알파벳, 숫자, 특수문자를 포함해야 합니다.") + private String newPassword; + + @NotBlank(message = "새 비밀번호 확인은 필수입니다.") + private String confirmPassword; +} \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserProfileUpdateDto.java b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserProfileUpdateDto.java new file mode 100644 index 0000000..3304649 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserProfileUpdateDto.java @@ -0,0 +1,22 @@ +package com.hyetaekon.hyetaekon.user.dto; + +import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +// 개인정보수정 - 그 외 정보 변경 항목 +@Getter +@Builder +public class UserProfileUpdateDto { + @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,8}$", message = "닉네임은 알파벳, 숫자, 한글만 포함할 수 있습니다.") + private String nickname; + + private String name; // 이름 + private LocalDate birthAt; // 생년월일 + private String gender; // 성별(남자/여자) + private String city; // 지역(시/도) + private String state; // 지역(시/군/구) + private String job; // 직업 +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserResponseDto.java index f712305..be92f79 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserResponseDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserResponseDto.java @@ -20,6 +20,7 @@ public class UserResponseDto { private String gender; // 성별 private String city; // 시/도 private String state; // 시/군/구 + private String job; // 직업 private String levelName; // 회원 등급 private int point; // 회원 포인트 diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserSignUpRequestDto.java b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserSignUpRequestDto.java index 45d129e..ca14714 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserSignUpRequestDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserSignUpRequestDto.java @@ -24,6 +24,9 @@ public class UserSignUpRequestDto { message = "비밀번호는 8자 이상 20자 이하여야 하며, 알파벳, 숫자, 특수문자를 포함해야 합니다.") private String password; // 평문 비밀번호 + @NotBlank(message = "비밀번호 확인은 공백일 수 없습니다.") + private String confirmPassword; + @NotBlank(message = "이름은 공백일 수 없습니다.") private String name; @@ -42,4 +45,6 @@ public class UserSignUpRequestDto { private String city; @NotNull(message = "지역(시/군/구)은 공백일 수 없습니다.") private String state; + + private String job; // 직업 } diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserUpdateRequestDto.java b/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserUpdateRequestDto.java deleted file mode 100644 index e445e59..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/user/dto/UserUpdateRequestDto.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.hyetaekon.hyetaekon.user.dto; - -import jakarta.validation.constraints.Pattern; -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDate; - -@Getter -@Builder -public class UserUpdateRequestDto { - - @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,8}$", message = "닉네임은 알파벳, 숫자, 한글만 포함할 수 있습니다.") - private String nickname; // 닉네임은 공백이나 null이 될 수 있음 - - private String currentPassword; // 현재 비밀번호 (비밀번호 변경이 필요할 때만 필수) - - @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$", - message = "비밀번호는 8자 이상 20자 이하여야 하며, 알파벳, 숫자, 특수문자를 포함해야 합니다.") - private String newPassword; // 새 비밀번호 (변경하지 않는 경우 null 또는 공백) - - private String name; // 이름 - - private LocalDate birthAt; // 생년월일 - - private String gender; // 성별 - - private String city; // 지역(시/도) - private String state; // 지역(시/군/구) -} \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/entity/User.java b/src/main/java/com/hyetaekon/hyetaekon/user/entity/User.java index 4aae716..babb41b 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/user/entity/User.java +++ b/src/main/java/com/hyetaekon/hyetaekon/user/entity/User.java @@ -134,6 +134,11 @@ public void updateState(String newState) { this.state = newState; } + // 회원 직업 정보 변경 + public void updateJob(String newJob) { + this.job = newJob; + } + // 회원 등급 Enum 변경 public void updateLevel(UserLevel level) { this.level = level; diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/service/UserService.java b/src/main/java/com/hyetaekon/hyetaekon/user/service/UserService.java index 7ad29c0..6c47b6c 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/user/service/UserService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/user/service/UserService.java @@ -4,12 +4,10 @@ import com.hyetaekon.hyetaekon.common.exception.GlobalException; import com.hyetaekon.hyetaekon.common.jwt.BlacklistService; import com.hyetaekon.hyetaekon.common.jwt.RefreshTokenService; -import com.hyetaekon.hyetaekon.user.dto.UserResponseDto; -import com.hyetaekon.hyetaekon.user.dto.UserSignUpRequestDto; -import com.hyetaekon.hyetaekon.user.dto.UserSignUpResponseDto; -import com.hyetaekon.hyetaekon.user.dto.UserUpdateRequestDto; +import com.hyetaekon.hyetaekon.user.dto.*; import com.hyetaekon.hyetaekon.user.entity.Role; import com.hyetaekon.hyetaekon.user.entity.User; +import com.hyetaekon.hyetaekon.user.entity.UserLevel; import com.hyetaekon.hyetaekon.user.mapper.UserMapper; import com.hyetaekon.hyetaekon.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -33,7 +31,7 @@ public class UserService { // 회원 가입 - // TODO: Occupation, BusinessType 재확인 + // TODO: Occupation, BusinessType 직업 정보 재확인 @Transactional public UserSignUpResponseDto registerUser(UserSignUpRequestDto userSignUpRequestDto) { // 이메일 또는 닉네임 중복 검사 @@ -42,6 +40,11 @@ public UserSignUpResponseDto registerUser(UserSignUpRequestDto userSignUpRequest userSignUpRequestDto.getNickname() ); + // 비밀번호와 비밀번호 확인 일치 여부 검사 + if (!userSignUpRequestDto.getPassword().equals(userSignUpRequestDto.getConfirmPassword())) { + throw new GlobalException(ErrorCode.PASSWORD_CONFIRM_MISMATCH); + } + if (existingUser.isPresent()) { User user = existingUser.get(); // NPE 방지 if (user.getRealId().equals(userSignUpRequestDto.getRealId())) { @@ -64,7 +67,9 @@ public UserSignUpResponseDto registerUser(UserSignUpRequestDto userSignUpRequest .gender(userSignUpRequestDto.getGender()) .city(userSignUpRequestDto.getCity()) .state(userSignUpRequestDto.getState()) + .job(userSignUpRequestDto.getJob()) .role(Role.ROLE_USER) + .level(UserLevel.QUESTION_MARK) .point(0) // 초기 포인트 설정 .createdAt(LocalDateTime.now()) // 생성 시간 설정 .build(); @@ -93,79 +98,82 @@ public User findUserByRealId(String realId) { } - // 회원 정보 수정(닉네임, 비밀번호, 이름, 성별, 생년월일, 지역) + // 회원 정보 수정(닉네임, 이름, 성별, 생년월일, 지역, 직업) @Transactional - public UserResponseDto updateUser(Long userId, UserUpdateRequestDto userUpdateRequestDto) { - // 사용자 정보 조회 + public UserResponseDto updateUserProfile(Long userId, UserProfileUpdateDto profileUpdateDto) { User user = userRepository.findByIdAndDeletedAtIsNull(userId) .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); - // 닉네임 변경 - String newNickname = userUpdateRequestDto.getNickname(); - if (newNickname != null && !newNickname.isBlank()) { // 닉네임이 null 또는 공백이 아닐 때만 처리 - if (userRepository.existsByNickname(newNickname)) { + // 닉네임 업데이트 + if (profileUpdateDto.getNickname() != null && !profileUpdateDto.getNickname().isBlank()) { + // 현재 닉네임과 다를 경우에만 중복 체크 + if (!user.getNickname().equals(profileUpdateDto.getNickname()) && + userRepository.existsByNickname(profileUpdateDto.getNickname())) { throw new GlobalException(ErrorCode.DUPLICATED_NICKNAME); } - user.updateNickname(newNickname); // 닉네임 변경 + user.updateNickname(profileUpdateDto.getNickname()); } - // 이름 변경 - String newName = userUpdateRequestDto.getName(); - if (newName != null && !newName.isBlank()) { - user.updateName(newName); + // 이름 업데이트 + if (profileUpdateDto.getName() != null && !profileUpdateDto.getName().isBlank()) { + user.updateName(profileUpdateDto.getName()); } - // 생년월일 변경 - if (userUpdateRequestDto.getBirthAt() != null) { - user.updateBirthAt(userUpdateRequestDto.getBirthAt()); + // 생년월일 업데이트 + if (profileUpdateDto.getBirthAt() != null) { + user.updateBirthAt(profileUpdateDto.getBirthAt()); } - // 성별 변경 - String newGender = userUpdateRequestDto.getGender(); - if (newGender != null && !newGender.isBlank()) { - user.updateGender(newGender); + // 성별 업데이트 + if (profileUpdateDto.getGender() != null && !profileUpdateDto.getGender().isBlank()) { + user.updateGender(profileUpdateDto.getGender()); } - // 지역 변경 - String newCity = userUpdateRequestDto.getCity(); - if (newCity != null && !newCity.isBlank()) { - user.updateCity(newCity); + // 지역(시/도) 업데이트 + if (profileUpdateDto.getCity() != null && !profileUpdateDto.getCity().isBlank()) { + user.updateCity(profileUpdateDto.getCity()); } - // 지역 변경 - String newState = userUpdateRequestDto.getState(); - if (newState != null && !newState.isBlank()) { - user.updateState(newState); + // 지역(시/군/구) 업데이트 + if (profileUpdateDto.getState() != null && !profileUpdateDto.getState().isBlank()) { + user.updateState(profileUpdateDto.getState()); } - // 비밀번호 변경 - String currentPassword = userUpdateRequestDto.getCurrentPassword(); - String newPassword = userUpdateRequestDto.getNewPassword(); + // 직업 업데이트 (필요시) + if (profileUpdateDto.getJob() != null && !profileUpdateDto.getJob().isBlank()) { + user.updateJob(profileUpdateDto.getJob()); + } - // 새 비밀번호가 있을 때만 비밀번호 변경 로직 실행 - if (newPassword != null && !newPassword.isBlank()) { - // 현재 비밀번호가 입력되지 않았으면 에러 발생 - if (currentPassword == null || currentPassword.isBlank()) { - throw new GlobalException(ErrorCode.CURRENT_PASSWORD_REQUIRED); - } + User updatedUser = userRepository.save(user); + log.debug("회원 프로필 정보 업데이트 - ID: {}", userId); + return userMapper.toResponseDto(updatedUser); + } - // 현재 비밀번호가 맞는지 확인 - if (!passwordEncoder.matches(currentPassword, user.getPassword())) { - throw new GlobalException(ErrorCode.PASSWORD_MISMATCH); - } + // 비밀번호 변경 + @Transactional + public void updateUserPassword(Long userId, UserPasswordUpdateDto passwordUpdateDto) { + User user = userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); - // 새 비밀번호가 기존 비밀번호와 같으면 에러 발생 - if (currentPassword.equals(newPassword)) { - throw new GlobalException(ErrorCode.PASSWORD_SAME_AS_OLD); - } + // 현재 비밀번호 검증 + if (!passwordEncoder.matches(passwordUpdateDto.getCurrentPassword(), user.getPassword())) { + throw new GlobalException(ErrorCode.PASSWORD_MISMATCH); + } - user.updatePassword(passwordEncoder.encode(newPassword)); // 비밀번호 변경 + // 새 비밀번호와 확인 비밀번호 일치 여부 확인 + if (!passwordUpdateDto.getNewPassword().equals(passwordUpdateDto.getConfirmPassword())) { + throw new GlobalException(ErrorCode.PASSWORD_CONFIRM_MISMATCH); } - // 변경된 사용자 정보 저장 - User updatedUser = userRepository.save(user); - log.debug("회원 정보 업데이트 - 이메일: {}", updatedUser.getRealId()); - return userMapper.toResponseDto(updatedUser); + // 새 비밀번호가 현재 비밀번호와 같은지 확인 + if (passwordEncoder.matches(passwordUpdateDto.getNewPassword(), user.getPassword())) { + throw new GlobalException(ErrorCode.PASSWORD_SAME_AS_OLD); + } + + // 비밀번호 업데이트 + user.updatePassword(passwordEncoder.encode(passwordUpdateDto.getNewPassword())); + userRepository.save(user); + log.debug("회원 비밀번호 변경 완료 - ID: {}", userId); } // 회원 탈퇴 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc01456..26750a1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,7 +19,7 @@ spring: jpa: show-sql: true # SQL 로그 출력 hibernate: - ddl-auto: create # update + ddl-auto: update # create # update properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect