Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public enum AuthErrorCode implements BaseErrorCode {
NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH-002", "사용자를 찾을 수 없습니다."),
DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "AUTH-003", "이미 존재하는 닉네임입니다."),
DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "AUTH-004", "이미 존재하는 아이디입니다."),
PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH-005", "새 비밀번호가 일치하지 않습니다."),
CURRENT_PASSWORD_REQUIRED(HttpStatus.BAD_REQUEST, "AUTH-006", "비밀번호 변경 시 현재 비밀번호가 필요합니다.")

;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.cokerthon.Team3_Backend.domain.user.controller;

import com.cokerthon.Team3_Backend.domain.user.dto.request.UserUpdateRequest;
import com.cokerthon.Team3_Backend.domain.user.dto.response.UserProfileResponse;
import com.cokerthon.Team3_Backend.domain.user.dto.response.UserUpdateResponse;
import com.cokerthon.Team3_Backend.domain.user.entity.User;
import com.cokerthon.Team3_Backend.domain.user.service.UserService;
import com.cokerthon.Team3_Backend.global.apiPayload.ApiResponse;
import com.cokerthon.Team3_Backend.global.apiPayload.code.GeneralSuccessCode;
import com.cokerthon.Team3_Backend.global.security.CurrentUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
@Tag(name = "User API", description = "유저 관련 API (프로필 조회, 수정, 아이디/닉네임 중복 확인)")
public class UserController {

private final UserService userService;

@PatchMapping("/me")
@Operation(summary = "개인정보 수정 API", description = "로그인한 사용자의 닉네임과 비밀번호를 수정합니다. 변경하지 않는 필드만 포함해 요청을 보냅니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "현재 비밀번호 불일치"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 존재하는 닉네임")
})
public ApiResponse<UserUpdateResponse> updateProfile(
@RequestBody @Valid UserUpdateRequest request,
@CurrentUser User user
) {
UserUpdateResponse response = userService.updateUser(user, request);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, response);
}

@GetMapping("/me")
@Operation(summary = "개인정보 조회 API", description = "마이페이지 개인정보 수정 페이지에서 로그인한 사용자의 아이디와 닉네임을 조회합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공")
})
public ApiResponse<UserProfileResponse> getProfile(@CurrentUser User user) {
UserProfileResponse response = userService.getProfile(user);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, response);
}

@GetMapping("/check-nickname")
@Operation(summary = "닉네임 중복 확인 API", description = "닉네임 중복 여부를 확인합니다. (true: 중복, false: 사용가능)")
public ApiResponse<Boolean> checkNickname(@RequestParam String nickname) {
boolean isDuplicate = userService.checkNicknameDuplicate(nickname);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, isDuplicate);
}

@GetMapping("/check-login-id")
@Operation(summary = "아이디 중복 확인 API", description = "아이디 중복 여부를 확인합니다. (true: 중복, false: 사용가능)")
public ApiResponse<Boolean> checkLoginId(@RequestParam String loginId) {
boolean isDuplicate = userService.checkLoginIdDuplicate(loginId);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, isDuplicate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.cokerthon.Team3_Backend.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

public record UserUpdateRequest(

@Schema(description = "변경할 닉네임 (변경하지 않을 경우 기존 닉네임으로 요청)", example = "달빛산책자")
String nickname,

@Schema(description = "현재 비밀번호", example = "password")
String currentPassword,

@Schema(description = "새 비밀번호", example = "newPassword")
String newPassword,

@Schema(description = "새 비밀번호 확인", example = "newPassword")
String confirmNewPassword
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cokerthon.Team3_Backend.domain.user.dto.response;

import com.cokerthon.Team3_Backend.domain.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;

public record UserProfileResponse(

@Schema(description = "로그인 ID", example = "user123")
String loginId,

@Schema(description = "닉네임", example = "달빛 산책자")
String nickname
) {
public static UserProfileResponse from(User user) {
return new UserProfileResponse(
user.getLoginId(),
user.getNickname()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.cokerthon.Team3_Backend.domain.user.dto.response;

import com.cokerthon.Team3_Backend.domain.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;

public record UserUpdateResponse(

@Schema(description = "유저 ID", example = "1")
Long id,

@Schema(description = "로그인 ID", example = "user123")
String loginId,

@Schema(description = "닉네임", example = "달빛 산책자")
String nickname
) {
public static UserUpdateResponse from(User user) {
return new UserUpdateResponse(
user.getId(),
user.getLoginId(),
user.getNickname()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ public class User extends BaseEntity {
/** 닉네임 */
@Column(nullable = false, length = 256, unique = true)
private String nickname;

public void updateNickname(String nickname) {
this.nickname = nickname;
}

public void updatePassword(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByLoginId(String loginId);

Optional<User> findByNickname(String nickname); }
Optional<User> findByNickname(String nickname);

boolean existsByNickname(String nickname);

boolean existsByLoginId(String loginId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.cokerthon.Team3_Backend.domain.user.service;

import com.cokerthon.Team3_Backend.domain.auth.exception.code.AuthErrorCode;
import com.cokerthon.Team3_Backend.domain.auth.exception.AuthException;
import com.cokerthon.Team3_Backend.domain.user.dto.request.UserUpdateRequest;
import com.cokerthon.Team3_Backend.domain.user.dto.response.UserProfileResponse;
import com.cokerthon.Team3_Backend.domain.user.dto.response.UserUpdateResponse;
import com.cokerthon.Team3_Backend.domain.user.entity.User;
import com.cokerthon.Team3_Backend.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public UserUpdateResponse updateUser(User user, UserUpdateRequest request) {

User managedUser = userRepository.findById(user.getId())
.orElseThrow(() -> new AuthException(AuthErrorCode.NOT_FOUND));

// 닉네임 변경
if (request.nickname() != null && !request.nickname().trim().isEmpty()
&& !request.nickname().trim().equals(managedUser.getNickname())) {
// 닉네임 중복 체크
if (userRepository.findByNickname(request.nickname().trim()).isPresent()) {
throw new AuthException(AuthErrorCode.DUPLICATE_NICKNAME);
}
managedUser.updateNickname(request.nickname().trim());
}

// 비밀번호 변경
if (request.newPassword() != null && !request.newPassword().isEmpty()) {
// 현재 비밀번호 확인 (비밀번호 변경 시 필수)
if (request.currentPassword() == null || request.currentPassword().isEmpty()) {
throw new AuthException(AuthErrorCode.CURRENT_PASSWORD_REQUIRED);
}
if (!passwordEncoder.matches(request.currentPassword(), managedUser.getPassword())) {
throw new AuthException(AuthErrorCode.UNAUTHORIZED);
}

// 새 비밀번호와 확인 비밀번호 일치 확인
if (!request.newPassword().equals(request.confirmNewPassword())) {
throw new AuthException(AuthErrorCode.PASSWORD_MISMATCH);
}

String encodedPassword = passwordEncoder.encode(request.newPassword());
managedUser.updatePassword(encodedPassword);
}

return UserUpdateResponse.from(managedUser);
}

public UserProfileResponse getProfile(User user) {
// DB에서 최신 정보 조회
User managedUser = userRepository.findById(user.getId())
.orElseThrow(() -> new AuthException(AuthErrorCode.NOT_FOUND));
return UserProfileResponse.from(managedUser);
}

public boolean checkNicknameDuplicate(String nickname) {
return userRepository.existsByNickname(nickname);
}

public boolean checkLoginIdDuplicate(String loginId) {
return userRepository.existsByLoginId(loginId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/api/auth/signup").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/users/check-nickname", "/api/users/check-login-id").permitAll()
//.anyRequest().permitAll()
.anyRequest().authenticated()
)
Expand Down