Skip to content

Commit f9d17a2

Browse files
authored
Merge pull request #38 from 9roomMoa/feat/#37
Feat/#37: 유저 프로필 수정 기능
2 parents 0be2ed7 + 9007071 commit f9d17a2

10 files changed

Lines changed: 278 additions & 5 deletions

File tree

src/main/java/com/groommoa/aether_back_spring/domain/user/entity/Member.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.groommoa.aether_back_spring.domain.user.entity;
22

33
import jakarta.persistence.*;
4-
import lombok.AccessLevel;
5-
import lombok.Builder;
6-
import lombok.Getter;
7-
import lombok.NoArgsConstructor;
4+
import lombok.*;
85
import org.springframework.data.annotation.CreatedDate;
96
import org.springframework.data.annotation.LastModifiedDate;
107
import org.springframework.data.mongodb.core.mapping.Document;
@@ -14,6 +11,7 @@
1411

1512
@Document(collection = "users")
1613
@Getter
14+
@Setter
1715
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1816
public class Member {
1917

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.groommoa.aether_back_spring.domain.user.service;
2+
3+
import com.groommoa.aether_back_spring.domain.user.entity.Member;
4+
import com.groommoa.aether_back_spring.domain.user.exception.UserException;
5+
import com.groommoa.aether_back_spring.domain.user.repository.UserRepository;
6+
import com.groommoa.aether_back_spring.global.auth.dto.UpdateUserProfileRequestDto;
7+
import com.groommoa.aether_back_spring.global.common.exception.ErrorCode;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class UserService {
14+
15+
private final UserRepository userRepository;
16+
17+
public Member updateUserProfile(String userId, UpdateUserProfileRequestDto request){
18+
Member member = userRepository.findById(userId)
19+
.orElseThrow(() -> new UserException(ErrorCode.USER_NOT_FOUND));
20+
if (request.getName() != null) member.setName(request.getName());
21+
if (request.getRank() != null) member.setRank(request.getRank());
22+
23+
return userRepository.save(member);
24+
}
25+
}

src/main/java/com/groommoa/aether_back_spring/global/auth/controller/AuthController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package com.groommoa.aether_back_spring.global.auth.controller;
22

33
import com.groommoa.aether_back_spring.domain.user.entity.Member;
4+
import com.groommoa.aether_back_spring.global.auth.dto.UpdateUserProfileRequestDto;
5+
import com.groommoa.aether_back_spring.global.auth.dto.UpdateUserProfileResponseDto;
6+
import com.groommoa.aether_back_spring.global.auth.service.AuthService;
47
import com.groommoa.aether_back_spring.global.auth.service.TokenService;
58
import com.groommoa.aether_back_spring.global.common.constants.HttpStatus;
69
import com.groommoa.aether_back_spring.global.common.constants.TokenKey;
710
import com.groommoa.aether_back_spring.global.common.response.CommonResponse;
11+
import com.groommoa.aether_back_spring.global.common.utils.DtoUtils;
812
import io.jsonwebtoken.Claims;
913
import jakarta.servlet.http.HttpServletResponse;
1014
import jakarta.servlet.http.HttpSession;
15+
import jakarta.validation.Valid;
1116
import lombok.RequiredArgsConstructor;
1217
import org.springframework.beans.factory.annotation.Value;
1318
import org.springframework.http.HttpHeaders;
@@ -33,6 +38,7 @@
3338
public class AuthController {
3439

3540
private final TokenService tokenService;
41+
private final AuthService authService;
3642

3743
/**
3844
* 소셜 로그인 성공시 호출됨 (redirect)
@@ -115,6 +121,18 @@ public ResponseEntity<CommonResponse> reissueAccessToken(HttpServletResponse res
115121
HttpStatus.OK, "access token 재발급에 성공했습니다.", result));
116122
}
117123

124+
@PatchMapping("/profile")
125+
public ResponseEntity<CommonResponse> updateUserProfile(@RequestBody @Valid UpdateUserProfileRequestDto request,
126+
@AuthenticationPrincipal Claims claims){
127+
Member updatedMember = authService.updateUserProfile(claims.getSubject(), request);
128+
UpdateUserProfileResponseDto responseDto = new UpdateUserProfileResponseDto(updatedMember);
129+
130+
CommonResponse response = new CommonResponse(
131+
HttpStatus.OK, "유저 프로필 정보 수정에 성공했습니다.", DtoUtils.toMap(responseDto));
132+
return ResponseEntity.ok(response);
133+
}
134+
135+
118136
/**
119137
* 테스트용 엔드포인트
120138
*
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.groommoa.aether_back_spring.global.auth.dto;
2+
3+
import com.groommoa.aether_back_spring.domain.user.entity.Rank;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public class UpdateUserProfileRequestDto {
8+
9+
private String name;
10+
private Rank rank;
11+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.groommoa.aether_back_spring.global.auth.dto;
2+
3+
import com.groommoa.aether_back_spring.domain.user.entity.Member;
4+
import com.groommoa.aether_back_spring.domain.user.entity.Rank;
5+
import com.groommoa.aether_back_spring.domain.user.entity.Role;
6+
import lombok.Getter;
7+
8+
import java.time.Instant;
9+
10+
@Getter
11+
public class UpdateUserProfileResponseDto {
12+
13+
private final String name;
14+
private final Role role;
15+
private final Rank rank;
16+
private final Instant createdAt;
17+
private final Instant updatedAt;
18+
19+
public UpdateUserProfileResponseDto(Member member) {
20+
this.name = member.getName();
21+
this.role = member.getRole();
22+
this.rank = member.getRank();
23+
this.createdAt = member.getCreatedAt();
24+
this.updatedAt = member.getUpdatedAt();
25+
}
26+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.groommoa.aether_back_spring.global.auth.service;
2+
3+
import com.groommoa.aether_back_spring.domain.user.entity.Member;
4+
import com.groommoa.aether_back_spring.domain.user.service.UserService;
5+
import com.groommoa.aether_back_spring.global.auth.dto.UpdateUserProfileRequestDto;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Service;
8+
9+
@Service
10+
@RequiredArgsConstructor
11+
public class AuthService {
12+
13+
private final UserService userService;
14+
15+
public Member updateUserProfile(String userId, UpdateUserProfileRequestDto request){
16+
return userService.updateUserProfile(userId, request);
17+
}
18+
}

src/main/java/com/groommoa/aether_back_spring/global/common/constants/HttpStatus.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
public final class HttpStatus {
44

55
public static final int OK = 200;
6-
public static final int UNAUTHORIZED = 401;
76
public static final int MOVED_PERMANENTLY = 301;
7+
public static final int BAD_REQUEST = 400;
8+
public static final int UNAUTHORIZED = 401;
9+
public static final int INTERNAL_SERVER_ERROR = 500;
810
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.groommoa.aether_back_spring.global.common.handler;
2+
3+
import com.fasterxml.jackson.core.JsonParseException;
4+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
5+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
6+
import com.groommoa.aether_back_spring.global.common.constants.HttpStatus;
7+
import com.groommoa.aether_back_spring.global.common.response.ErrorResponse;
8+
import com.mongodb.MongoException;
9+
import org.apache.commons.lang.exception.ExceptionUtils;
10+
import org.bson.types.ObjectId;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.http.converter.HttpMessageNotReadableException;
13+
import org.springframework.web.bind.MethodArgumentNotValidException;
14+
import org.springframework.web.bind.annotation.ExceptionHandler;
15+
import org.springframework.web.bind.annotation.RestControllerAdvice;
16+
17+
import java.util.*;
18+
19+
@RestControllerAdvice
20+
public class GlobalExceptionHandler {
21+
22+
@ExceptionHandler(HttpMessageNotReadableException.class)
23+
public ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
24+
Throwable cause = ExceptionUtils.getRootCause(ex);
25+
if (cause == null) cause = ex.getCause();
26+
27+
String message = "유효하지 않은 입력값입니다.";
28+
List<Map<String, String>> errors = new ArrayList<>();
29+
30+
if (cause instanceof InvalidFormatException ife) {
31+
Class<?> targetType = ife.getTargetType();
32+
Object invalidValue = ife.getValue();
33+
String field = extractFieldFromPath(ife.getPathReference());
34+
35+
if (targetType.isEnum()) {
36+
message = String.format("'%s'은(는) 지원하지 않는 값입니다.", invalidValue);
37+
errors.add(Map.of(
38+
"field", field,
39+
"reason", String.format("가능한 값: %s", Arrays.toString(targetType.getEnumConstants()))
40+
));
41+
} else if (targetType == ObjectId.class) {
42+
message = "잘못된 ObjectId 형식입니다.";
43+
errors.add(Map.of(
44+
"field", field,
45+
"reason", String.format("'%s'은(는) 올바르지 않은 ObjectId입니다.", invalidValue)
46+
));
47+
} else {
48+
message = "입력값 형식 오류";
49+
errors.add(Map.of(
50+
"field", field,
51+
"reason", String.format("'%s'은(는) %s 타입으로 변환할 수 없습니다.",
52+
invalidValue, targetType.getSimpleName())
53+
));
54+
}
55+
} else if (cause instanceof IllegalArgumentException iae && iae.getMessage() != null) {
56+
String msg = iae.getMessage();
57+
if (msg.contains("No enum constant")) {
58+
try {
59+
String[] parts = msg.split("No enum constant ")[1].split("\\.");
60+
String className = String.join(".", Arrays.copyOf(parts, parts.length - 1));
61+
String invalidValue = parts[parts.length - 1];
62+
String simpleClassName = className.substring(className.lastIndexOf(".") + 1);
63+
64+
message = "Enum 값이 올바르지 않습니다.";
65+
errors.add(Map.of(
66+
"field", simpleClassName,
67+
"reason", String.format("'%s'은(는) %s에서 지원하지 않는 Enum 값입니다. 대소문자나 오타를 확인해주세요.",
68+
invalidValue, simpleClassName)
69+
));
70+
} catch (Exception e) {
71+
message = "Enum 값이 올바르지 않습니다.";
72+
}
73+
} else {
74+
message = msg;
75+
}
76+
} else if (cause instanceof MismatchedInputException) {
77+
message = "입력값의 형식이 맞지 않습니다.";
78+
} else if (cause instanceof JsonParseException) {
79+
message = "잘못된 JSON 형식입니다.";
80+
} else message = Objects.requireNonNullElse(cause, ex).getMessage();
81+
82+
Map<String, Object> details = errors.isEmpty() ? null : Map.of("errors", errors);
83+
ErrorResponse response = new ErrorResponse(HttpStatus.BAD_REQUEST, message, details);
84+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
85+
}
86+
87+
@ExceptionHandler(MethodArgumentNotValidException.class)
88+
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
89+
List<Map<String, String>> errors = ex.getBindingResult().getFieldErrors().stream()
90+
.map(error -> Map.of(
91+
"field", error.getField(),
92+
"reason", error.getDefaultMessage()
93+
))
94+
.toList();
95+
96+
Map<String, Object> details = Map.of("errors", errors);
97+
98+
ErrorResponse response = new ErrorResponse(
99+
HttpStatus.BAD_REQUEST, "유효하지 않은 입력값입니다.", details
100+
);
101+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
102+
}
103+
104+
105+
@ExceptionHandler(MongoException.class)
106+
public ResponseEntity<ErrorResponse> handleMongoException(MongoException ex) {
107+
ex.printStackTrace();
108+
ErrorResponse response = new ErrorResponse(
109+
HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 오류가 발생했습니다.", null);
110+
111+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
112+
}
113+
114+
@ExceptionHandler(Exception.class)
115+
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
116+
ex.printStackTrace();
117+
ErrorResponse response = new ErrorResponse(
118+
HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다.", null);
119+
120+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
121+
}
122+
123+
private String extractFieldFromPath(String pathReference) {
124+
if (pathReference == null) return "unknown";
125+
int start = pathReference.lastIndexOf("[\"") + 2;
126+
int end = pathReference.lastIndexOf("\"]");
127+
if (start > 1 && end > start) {
128+
return pathReference.substring(start, end);
129+
}
130+
return "unknown";
131+
}
132+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.groommoa.aether_back_spring.global.common.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
import java.util.Map;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public class ErrorResponse {
11+
12+
private final int code;
13+
private final String message;
14+
private final Map<String, Object> details;
15+
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.groommoa.aether_back_spring.global.common.utils;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import jakarta.annotation.PostConstruct;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.util.Map;
10+
11+
@Component
12+
public class DtoUtils {
13+
14+
@Autowired
15+
private ObjectMapper injectedObjectMapper;
16+
17+
private static ObjectMapper objectMapper;
18+
19+
@PostConstruct
20+
public void init() {
21+
objectMapper = injectedObjectMapper;
22+
}
23+
24+
public static Map<String, Object> toMap(Object dto){
25+
return objectMapper.convertValue(dto, new TypeReference<>() {});
26+
}
27+
}

0 commit comments

Comments
 (0)