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
14 changes: 14 additions & 0 deletions src/main/java/com/back/domain/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
public class UserController implements UserControllerDocs {
private final UserService userService;

// 내 정보 조회
@GetMapping("/me")
public ResponseEntity<RsData<UserDetailResponse>> getMyInfo (
@AuthenticationPrincipal CustomUserDetails user
Expand All @@ -29,6 +30,7 @@ public ResponseEntity<RsData<UserDetailResponse>> getMyInfo (
));
}

// 내 정보 수정
@PatchMapping("/me")
public ResponseEntity<RsData<UserDetailResponse>> updateMyProfile(
@AuthenticationPrincipal CustomUserDetails user,
Expand All @@ -42,4 +44,16 @@ public ResponseEntity<RsData<UserDetailResponse>> updateMyProfile(
)
);
}

// 내 계정 삭제
@DeleteMapping("/me")
public ResponseEntity<RsData<Void>> deleteMyAccount(
@AuthenticationPrincipal CustomUserDetails user
) {
userService.deleteUser(user.getUserId());
return ResponseEntity
.ok(RsData.success(
"회원 탈퇴가 완료되었습니다."
));
}
}
123 changes: 121 additions & 2 deletions src/main/java/com/back/domain/user/controller/UserControllerDocs.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public interface UserControllerDocs {
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (Access Token 문제)",
description = "인증 실패 (토큰 없음/잘못됨/만료)",
content = @Content(
mediaType = "application/json",
examples = {
Expand All @@ -99,7 +99,7 @@ public interface UserControllerDocs {
"data": null
}
"""),
@ExampleObject(name = "유효하지 않은 토큰", value = """
@ExampleObject(name = "잘못된 토큰", value = """
{
"success": false,
"code": "AUTH_002",
Expand Down Expand Up @@ -303,4 +303,123 @@ ResponseEntity<RsData<UserDetailResponse>> updateMyProfile(
@AuthenticationPrincipal CustomUserDetails user,
@Valid @RequestBody UpdateUserProfileRequest request
);

@Operation(
summary = "내 계정 삭제",
description = "로그인한 사용자의 계정을 탈퇴 처리합니다. " +
"탈퇴 시 사용자 상태는 DELETED로 변경되며, 프로필 정보는 마스킹 처리됩니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "회원 탈퇴 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "회원 탈퇴가 완료되었습니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "403",
description = "정지된 계정",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "USER_008",
"message": "정지된 계정입니다. 관리자에게 문의하세요.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "410",
description = "탈퇴한 계정",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "USER_009",
"message": "탈퇴한 계정입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (토큰 없음/잘못됨/만료)",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "토큰 없음", value = """
{
"success": false,
"code": "AUTH_001",
"message": "인증이 필요합니다.",
"data": null
}
"""),
@ExampleObject(name = "잘못된 토큰", value = """
{
"success": false,
"code": "AUTH_002",
"message": "유효하지 않은 액세스 토큰입니다.",
"data": null
}
"""),
@ExampleObject(name = "만료된 토큰", value = """
{
"success": false,
"code": "AUTH_004",
"message": "만료된 액세스 토큰입니다.",
"data": null
}
""")
}
)
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 사용자",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "USER_001",
"message": "존재하지 않는 사용자입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
""")
)
)
})
ResponseEntity<RsData<Void>> deleteMyAccount(
@AuthenticationPrincipal CustomUserDetails user
);
}
78 changes: 56 additions & 22 deletions src/main/java/com/back/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,8 @@ public class UserService {
*/
public UserDetailResponse getUserInfo(Long userId) {

// userId로 User 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// UserStatus가 DELETED, SUSPENDED면 예외 처리
if (user.getUserStatus() == UserStatus.DELETED) {
throw new CustomException(ErrorCode.USER_DELETED);
}
if (user.getUserStatus() == UserStatus.SUSPENDED) {
throw new CustomException(ErrorCode.USER_SUSPENDED);
}
// 사용자 조회 및 상태 검증
User user = getValidUser(userId);

// UserDetailResponse로 변환하여 반환
return UserDetailResponse.from(user);
Expand All @@ -52,17 +43,8 @@ public UserDetailResponse getUserInfo(Long userId) {
*/
public UserDetailResponse updateUserProfile(Long userId, UpdateUserProfileRequest request) {

// userId로 User 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// UserStatus가 DELETED, SUSPENDED면 예외 처리
if (user.getUserStatus() == UserStatus.DELETED) {
throw new CustomException(ErrorCode.USER_DELETED);
}
if (user.getUserStatus() == UserStatus.SUSPENDED) {
throw new CustomException(ErrorCode.USER_SUSPENDED);
}
// 사용자 조회 및 상태 검증
User user = getValidUser(userId);

// 닉네임 중복 검사 (본인 제외)
if (userProfileRepository.existsByNicknameAndUserIdNot(request.nickname(), userId)) {
Expand All @@ -79,4 +61,56 @@ public UserDetailResponse updateUserProfile(Long userId, UpdateUserProfileReques
// UserDetailResponse로 변환하여 반환
return UserDetailResponse.from(user);
}

/**
* 사용자 탈퇴 서비스 (soft delete)
* 1. 사용자 조회 및 상태 검증
* 2. UserStatus를 DELETED로 변경
*/
public void deleteUser(Long userId) {

// 사용자 조회 및 상태 검증
User user = getValidUser(userId);

// 상태 변경 (soft delete)
user.setUserStatus(UserStatus.DELETED);

// 식별 정보 변경 (username, email, provider, providerId)
user.setUsername("deleted_" + user.getUsername());
user.setEmail("deleted_" + user.getEmail());
user.setProvider("deleted_" + user.getProvider());
user.setProviderId("deleted_" + user.getProviderId());

// 개인정보 마스킹
UserProfile profile = user.getUserProfile();
if (profile != null) {
profile.setNickname("탈퇴한 회원");
profile.setProfileImageUrl(null);
profile.setBio(null);
profile.setBirthDate(null);
}
}

/**
* 유효한 사용자 조회 및 상태 검증
*
* @param userId 사용자 ID
* @return user 조회된 사용자 엔티티
*/
private User getValidUser(Long userId) {

// userId로 User 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// UserStatus가 DELETED, SUSPENDED면 예외 처리
if (user.getUserStatus() == UserStatus.DELETED) {
throw new CustomException(ErrorCode.USER_DELETED);
}
if (user.getUserStatus() == UserStatus.SUSPENDED) {
throw new CustomException(ErrorCode.USER_SUSPENDED);
}

return user;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.back.domain.user.entity.User;
import com.back.domain.user.entity.UserProfile;
import com.back.domain.user.repository.UserProfileRepository;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
Expand Down
Loading