-
Notifications
You must be signed in to change notification settings - Fork 1
[feat] 사용자 정보 수정 api 개발 #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a2ec743
f2a57c6
472724c
4907e79
97d464b
f96b82c
df025d3
750c8b7
fd4f7c1
7180021
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||
| package konkuk.thip.user.adapter.in.web.request; | ||||||||||||
|
|
||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||||||||||
| import jakarta.validation.constraints.NotBlank; | ||||||||||||
| import konkuk.thip.user.application.port.in.dto.UserUpdateCommand; | ||||||||||||
|
|
||||||||||||
| @Schema(description = "사용자 정보 수정 요청 DTO") | ||||||||||||
| public record UserUpdateRequest( | ||||||||||||
|
|
||||||||||||
| @Schema(description = "사용자 수정 칭호", example = "문학가") | ||||||||||||
| @NotBlank(message = "칭호는 필수입니다.") | ||||||||||||
| String aliasName, | ||||||||||||
|
|
||||||||||||
| @Schema(description = "사용자 수정 닉네임", example = "thip") | ||||||||||||
| String nickname | ||||||||||||
|
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 닉네임 필드 검증 누락 aliasName에는 @notblank 검증이 있지만, nickname 필드에는 검증 어노테이션이 없습니다. 도메인 로직에서 닉네임 길이와 공백을 검증하고 있지만, API 레벨에서도 기본적인 검증을 추가하는 것이 좋겠습니다. @Schema(description = "사용자 수정 닉네임", example = "thip")
+ @Size(max = 10, message = "닉네임은 10자 이하여야 합니다.")
String nickname📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| ) { | ||||||||||||
| public UserUpdateCommand toCommand(Long userId) { | ||||||||||||
| return new UserUpdateCommand(aliasName, nickname, userId); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,8 @@ | |
| import konkuk.thip.common.entity.BaseJpaEntity; | ||
| import konkuk.thip.user.domain.User; | ||
| import lombok.*; | ||
| import org.springframework.util.Assert; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| @Entity | ||
| @Table(name = "users") | ||
|
|
@@ -23,6 +24,9 @@ public class UserJpaEntity extends BaseJpaEntity { | |
| @Column(length = 60, nullable = false) | ||
| private String nickname; | ||
|
|
||
| @Column(name = "nickname_updated_at", nullable = false) | ||
| private LocalDate nicknameUpdatedAt; // 날짜 형식으로 저장 (예: "2023-10-01") | ||
|
|
||
|
Comment on lines
+27
to
+29
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요구사항을 구현하려면 이게 있어야 하겠네요. 확인했습니다 |
||
| @Column(name = "oauth2_id", length = 50, nullable = false) | ||
| private String oauth2Id; | ||
|
|
||
|
|
@@ -37,9 +41,17 @@ public class UserJpaEntity extends BaseJpaEntity { | |
| @JoinColumn(name = "user_alias_id", nullable = false) | ||
| private AliasJpaEntity aliasForUserJpaEntity; | ||
|
|
||
| public void updateIncludeAliasFrom(User user, AliasJpaEntity aliasJpaEntity) { | ||
| this.nickname = user.getNickname(); | ||
| this.nicknameUpdatedAt = user.getNicknameUpdatedAt(); | ||
| this.role = UserRole.from(user.getUserRole()); | ||
| this.followerCount = user.getFollowerCount(); | ||
| this.aliasForUserJpaEntity = aliasJpaEntity; | ||
| } | ||
|
|
||
| public void updateFrom(User user) { | ||
| this.nickname = user.getNickname(); | ||
| Assert.notNull(user.getAlias(), "Alias must not be null"); | ||
| this.nicknameUpdatedAt = user.getNicknameUpdatedAt(); | ||
| this.role = UserRole.from(user.getUserRole()); | ||
| this.followerCount = user.getFollowerCount(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package konkuk.thip.user.application.port.in; | ||
|
|
||
| import konkuk.thip.user.application.port.in.dto.UserUpdateCommand; | ||
|
|
||
| public interface UserUpdateUseCase { | ||
| void updateUser(UserUpdateCommand command); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package konkuk.thip.user.application.port.in.dto; | ||
|
|
||
| public record UserUpdateCommand( | ||
| String aliasName, | ||
| String nickname, | ||
| Long userId | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package konkuk.thip.user.application.service; | ||
|
|
||
| import konkuk.thip.common.exception.BusinessException; | ||
| import konkuk.thip.common.exception.code.ErrorCode; | ||
| import konkuk.thip.user.application.port.in.UserUpdateUseCase; | ||
| import konkuk.thip.user.application.port.in.dto.UserUpdateCommand; | ||
| import konkuk.thip.user.application.port.out.UserCommandPort; | ||
| import konkuk.thip.user.application.port.out.UserQueryPort; | ||
| import konkuk.thip.user.domain.Alias; | ||
| import konkuk.thip.user.domain.User; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class UserUpdateService implements UserUpdateUseCase { | ||
|
|
||
| private final UserCommandPort userCommandPort; | ||
| private final UserQueryPort userQueryPort; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public void updateUser(UserUpdateCommand command) { | ||
| Alias alias = Alias.from(command.aliasName()); | ||
| boolean isNicknameUpdateRequest = command.nickname() != null; // 닉네임이 null이 아니면 닉네임 업데이트 요청 | ||
|
|
||
| User user = userCommandPort.findById(command.userId()); | ||
| user.updateUserInfo(command.nickname(), alias, isNicknameUpdateRequest); | ||
|
|
||
| if(isNicknameUpdateRequest && userQueryPort.existsByNicknameAndUserIdNot(command.nickname(), command.userId())) { | ||
| throw new BusinessException(ErrorCode.USER_NICKNAME_ALREADY_EXISTS); | ||
| } | ||
|
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 닉네임 중복 검사 순서 개선 필요 닉네임 중복 검사가 도메인 객체 검증 이후에 수행되고 있습니다. 하지만 도메인 검증에서 예외가 발생하면 중복 검사가 실행되지 않습니다. 논리적으로는 중복 검사를 먼저 수행하는 것이 더 효율적입니다. User user = userCommandPort.findById(command.userId());
+
+ if(isNicknameUpdateRequest && userQueryPort.existsByNicknameAndUserIdNot(command.nickname(), command.userId())) {
+ throw new BusinessException(ErrorCode.USER_NICKNAME_ALREADY_EXISTS);
+ }
+
user.updateUserInfo(command.nickname(), alias, isNicknameUpdateRequest);
- if(isNicknameUpdateRequest && userQueryPort.existsByNicknameAndUserIdNot(command.nickname(), command.userId())) {
- throw new BusinessException(ErrorCode.USER_NICKNAME_ALREADY_EXISTS);
- }🤖 Prompt for AI Agents |
||
| userCommandPort.update(user); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ | |
| import lombok.Getter; | ||
| import lombok.experimental.SuperBuilder; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| @Getter | ||
| @SuperBuilder | ||
| public class User extends BaseDomainEntity { | ||
|
|
@@ -14,6 +16,8 @@ public class User extends BaseDomainEntity { | |
|
|
||
| private String nickname; | ||
|
|
||
| private LocalDate nicknameUpdatedAt; | ||
|
|
||
| private String userRole; | ||
|
|
||
| private String oauth2Id; | ||
|
|
@@ -22,10 +26,11 @@ public class User extends BaseDomainEntity { | |
|
|
||
| private Alias alias; | ||
|
|
||
| public static User withoutId(String nickname, String userRole, String oauth2Id, Alias alias) { | ||
| public static User withoutId(String nickname, LocalDate nicknameUpdatedAt, String userRole, String oauth2Id, Alias alias) { | ||
| return User.builder() | ||
| .id(null) | ||
| .nickname(nickname) | ||
| .nicknameUpdatedAt(nicknameUpdatedAt) | ||
| .userRole(userRole) | ||
| .oauth2Id(oauth2Id) | ||
| .followerCount(0) | ||
|
|
@@ -44,4 +49,29 @@ public void decreaseFollowerCount() { | |
| followerCount--; | ||
| } | ||
|
|
||
| public void updateUserInfo(String nickname, Alias alias, boolean isNicknameUpdateRequest) { | ||
| if(isNicknameUpdateRequest) { | ||
| validateCanUpdateNickname(nickname); | ||
| this.nickname = nickname; | ||
| this.nicknameUpdatedAt = LocalDate.now(); | ||
| } | ||
| this.alias = alias; | ||
| } | ||
|
|
||
| private void validateCanUpdateNickname(String nickname) { | ||
| if(nickname.isBlank()) { // 빈칸 불가 | ||
| throw new InvalidStateException(ErrorCode.USER_NICKNAME_CANNOT_BE_BLANK); | ||
| } | ||
| if(nickname.length() > 10) { // 10자 이상 불가 | ||
| throw new InvalidStateException(ErrorCode.USER_NICKNAME_TOO_LONG); | ||
| } | ||
| // 닉네임을 변경한지 6개월이 지나지 않았으면 닉네임 업데이트 불가 | ||
| if(nicknameUpdatedAt != null && nicknameUpdatedAt.isAfter(LocalDate.now().minusMonths(6))) { | ||
| throw new InvalidStateException(ErrorCode.USER_NICKNAME_UPDATE_TOO_FREQUENT); | ||
| } | ||
| if(nickname.equals(this.nickname)) { // 현재 닉네임과 같으면 업데이트 불가 | ||
| throw new InvalidStateException(ErrorCode.USER_NICKNAME_CANNOT_BE_SAME); | ||
| } | ||
| } | ||
|
Comment on lines
+61
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 닉네임 검증 로직에서 null 체크를 강화해주세요. 현재 nicknameUpdatedAt가 null인 경우에 대한 처리가 있지만, 새로 생성되는 사용자의 경우를 더 명확하게 처리하는 것이 좋겠습니다. 다음과 같이 수정을 고려해보세요: private void validateCanUpdateNickname(String nickname) {
if(nickname.isBlank()) {
throw new InvalidStateException(ErrorCode.USER_NICKNAME_CANNOT_BE_BLANK);
}
if(nickname.length() > 10) {
throw new InvalidStateException(ErrorCode.USER_NICKNAME_TOO_LONG);
}
if(nickname.equals(this.nickname)) {
throw new InvalidStateException(ErrorCode.USER_NICKNAME_CANNOT_BE_SAME);
}
// 닉네임을 변경한지 6개월이 지나지 않았으면 닉네임 업데이트 불가
// null인 경우는 최초 생성 시점이므로 업데이트 허용
if(nicknameUpdatedAt != null && nicknameUpdatedAt.isAfter(LocalDate.now().minusMonths(6))) {
throw new InvalidStateException(ERROR.USER_NICKNAME_UPDATE_TOO_FREQUENT);
}
}또한 검증 순서를 재고려해보세요. 같은 닉네임 체크를 더 빠른 시점에 하는 것이 효율적일 수 있습니다. 🤖 Prompt for AI Agents
Comment on lines
+61
to
+75
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM |
||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p3 : 이렇게 @Schema 어노테이션을 추가하면 스웨거 상에서도 request dto의 description을 확인할 수 있는건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p3 : 추가로 nickname 이 10자 이하여야 한다는 것을 request dto 단에서 수행하는 것이 아니라, User 도메인 내부에서 수행하신 이유가 있을까요?? 닉네임이 10자 이하여야 한다 라는 도메인 규칙을 맞추기 위함인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 확인 가능합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 nickname은 pr 설명에서도 적어놓으셨듯이, null로 오는 경우가 존재해서 그 경우를 핸들링하기 위해 도메인에서 처리해주었습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 좋습니다! 저도 적용해보겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵넵 아무래도 도메인 규칙이 맞으니, 도메인 레벨에 검증하는 로직이 있는게 맞는거 같네요.
현재 request 에서 수행하는 bean validation 중 도메인 규칙에 해당하는 친구들은 도메인 레이어로 옮기는 것 또한 리펙토링 포인트로 생각해봐도 좋을 것 같네요! (수정해야하는게 도대체 몇개죠 허허)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ㅋㅋㅋㅋ좋습니다! 그 부분은 급한건 아니니 일단 미뤄보죠 🥲