Skip to content
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

feat: 회원 정보 수정, 회원 탈퇴 API 추가 #78

Merged
merged 6 commits into from
Jun 11, 2024
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
23 changes: 20 additions & 3 deletions api/src/main/java/vook/server/api/app/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import vook.server.api.app.user.data.OnboardingCommand;
import vook.server.api.app.user.data.RegisterCommand;
import vook.server.api.app.user.data.SignUpFromSocialCommand;
import vook.server.api.app.user.exception.AlreadyOnboardingException;
import vook.server.api.app.user.exception.AlreadyRegisteredException;
import vook.server.api.app.user.exception.NotReadyToOnboardingException;
import vook.server.api.app.user.exception.*;
import vook.server.api.app.user.repo.SocialUserRepository;
import vook.server.api.app.user.repo.UserInfoRepository;
import vook.server.api.app.user.repo.UserRepository;
Expand Down Expand Up @@ -49,6 +47,9 @@ public void register(RegisterCommand command) {
if (user.isRegistered()) {
throw new AlreadyRegisteredException();
}
if (user.isWithdrawn()) {
throw new WithdrawnUserException();
}

UserInfo userInfo = userInfoRepository.save(UserInfo.forRegisterOf(
command.getNickname(),
Expand All @@ -69,4 +70,20 @@ public void onboarding(OnboardingCommand command) {

user.onboarding(command.getFunnel(), command.getJob());
}

public void updateInfo(String uid, String nickname) {
User user = repository.findByUid(uid).orElseThrow();
if (!user.isRegistered()) {
throw new NotRegisteredException();
}
user.update(nickname);
}

public void withdraw(String uid) {
User user = repository.findByUid(uid).orElseThrow();
if (user.isWithdrawn()) {
return;
}
user.withdraw();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package vook.server.api.app.user.exception;

import vook.server.api.app.common.AppException;

public class NotRegisteredException extends AppException {
@Override
public String contents() {
return "NotRegistered";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package vook.server.api.app.user.exception;

import vook.server.api.app.common.AppException;

public class WithdrawnUserException extends AppException {
@Override
public String contents() {
return "WithdrawnUser";
}
}
16 changes: 15 additions & 1 deletion api/src/main/java/vook/server/api/model/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class User {

private LocalDateTime lastUpdatedAt;

private LocalDateTime deletedAt;
private LocalDateTime withdrawnAt;

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<SocialUser> socialUsers = new ArrayList<>();
Expand Down Expand Up @@ -76,4 +76,18 @@ public boolean isReadyToOnboarding() {
public boolean isRegistered() {
return status == UserStatus.REGISTERED;
}

public void update(String nickname) {
userInfo.update(nickname);
lastUpdatedAt = LocalDateTime.now();
}

public void withdraw() {
this.status = UserStatus.WITHDRAWN;
this.withdrawnAt = LocalDateTime.now();
}

public boolean isWithdrawn() {
return status == UserStatus.WITHDRAWN;
}
}
4 changes: 4 additions & 0 deletions api/src/main/java/vook/server/api/model/user/UserInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ public void addOnboardingInfo(Funnel funnel, Job job) {
this.funnel = funnel;
this.job = job;
}

public void update(String nickname) {
this.nickname = nickname;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package vook.server.api.helper.jwt;
package vook.server.api.web.auth.app;

import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import vook.server.api.helper.jwt.JWTReader;
import vook.server.api.helper.jwt.JWTWriter;

@Component
public class JWTHelperProvider {
Expand All @@ -19,7 +21,7 @@ public void init() {
jwtReaderBuilder = new JWTReader.Builder(jwtSecret);
}

public JWTWriter builder() {
public JWTWriter writer() {
return jwtWriterBuilder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import vook.server.api.helper.jwt.JWTHelperProvider;
import vook.server.api.helper.jwt.JWTReader;
import vook.server.api.web.auth.data.GeneratedToken;

Expand Down Expand Up @@ -50,15 +49,15 @@ public GeneratedToken refreshToken(String refreshToken) {
}

private String buildAccessToken(String uid) {
return jwtHelperProvider.builder()
return jwtHelperProvider.writer()
.withExpiredMs(1000L * 60 * accessTokenExpiredMinute)
.withClaim("category", "access")
.withClaim("uid", uid)
.jwtString();
}

private String buildRefreshToken(String uid) {
return jwtHelperProvider.builder()
return jwtHelperProvider.writer()
.withExpiredMs(1000L * 60 * refreshTokenExpiredMinute)
.withClaim("category", "refresh")
.withClaim("uid", uid)
Expand Down
35 changes: 34 additions & 1 deletion api/src/main/java/vook/server/api/web/routes/user/UserApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class UserApiUerInfoResponse extends CommonApiResponse<UserInfoResponse> {
description = """
비즈니스 규칙 위반 내용
- AlreadyRegistered: 이미 회원가입이 완료된 유저가 해당 API를 호출 할 경우
"""
- WithdrawnUser: 탈퇴한 유저가 해당 API를 호출 할 경우"""
)
@ApiResponses(value = {
@ApiResponse(
Expand Down Expand Up @@ -87,4 +87,37 @@ class UserApiUerInfoResponse extends CommonApiResponse<UserInfoResponse> {
),
})
CommonApiResponse<Void> onboarding(VookLoginUser user, UserOnboardingRequest request);

@Operation(
summary = "사용자 정보 수정",
security = {
@SecurityRequirement(name = "AccessToken")
},
description = """
비즈니스 규칙 위반 내용
- NotRegistered: 가입하지 않은 유저가 해당 API를 호출 할 경우"""
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "400",
content = @Content(
mediaType = "application/json",
schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE),
examples = {
@ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER),
@ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE)
}
)
),
})
CommonApiResponse<Void> updateInfo(VookLoginUser user, UserUpdateInfoRequest request);

@Operation(
summary = "회원 탈퇴",
security = {
@SecurityRequirement(name = "AccessToken")
},
description = "탈퇴된 회원에 대한 요청은 무시됩니다."
)
CommonApiResponse<Void> withdraw(VookLoginUser user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,23 @@ public CommonApiResponse<Void> onboarding(
service.onboarding(user, request);
return CommonApiResponse.ok();
}

@Override
@PutMapping("/info")
public CommonApiResponse<Void> updateInfo(
@AuthenticationPrincipal VookLoginUser user,
@Validated @RequestBody UserUpdateInfoRequest request
) {
service.updateInfo(user, request);
return CommonApiResponse.ok();
}

@Override
@PostMapping("/withdraw")
public CommonApiResponse<Void> withdraw(
@AuthenticationPrincipal VookLoginUser user
) {
service.withdraw(user);
return CommonApiResponse.ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package vook.server.api.web.routes.user;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class UserUpdateInfoRequest {

@NotBlank
@Size(min = 1, max = 10)
private String nickname;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ public void register(VookLoginUser loginUser, UserRegisterRequest request) {
public void onboarding(VookLoginUser loginUser, UserOnboardingRequest request) {
userService.onboarding(request.toCommand(loginUser.getUid()));
}

public void updateInfo(VookLoginUser loginUser, UserUpdateInfoRequest request) {
userService.updateInfo(loginUser.getUid(), request.getNickname());
}

public void withdraw(VookLoginUser loginUser) {
userService.withdraw(loginUser.getUid());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public User createCompletedOnboardingUser() {
return userService.findByUid(user.getUid()).orElseThrow();
}

public User createWithdrawnUser() {
User user = createCompletedOnboardingUser();
userService.withdraw(user.getUid());
return userService.findByUid(user.getUid()).orElseThrow();
}

public GeneratedToken createToken(User user) {
return tokenService.generateToken(user.getUid());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import vook.server.api.model.user.User;
import vook.server.api.testhelper.HttpEntityBuilder;
import vook.server.api.testhelper.IntegrationTestBase;
Expand All @@ -22,6 +23,7 @@

import static org.assertj.core.api.Assertions.assertThat;

@Transactional
class UserRestControllerTest extends IntegrationTestBase {

@MockBean
Expand Down Expand Up @@ -133,4 +135,59 @@ Collection<DynamicTest> registerError() {
})
);
}

@Test
@DisplayName("회원 정보 수정 - 정상")
void updateInfo() {
// given
User registeredUser = testDataCreator.createRegisteredUser();
GeneratedToken token = testDataCreator.createToken(registeredUser);

// when
var res = rest.exchange(
"/user/info",
HttpMethod.PUT,
new HttpEntityBuilder()
.header("Authorization", "Bearer " + token.getAccessToken())
.body(Map.of(
"nickname", "newName"
))
.build(),
String.class
);

// then
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
}

@TestFactory
@DisplayName("회원 정보 수정 - 실패")
Collection<DynamicTest> updateInfoError() {
// given
User registeredUser = testDataCreator.createRegisteredUser();
GeneratedToken token = testDataCreator.createToken(registeredUser);

Function<Map<String, Object>, ResponseEntity<String>> restExchange = body -> rest.exchange(
"/user/info",
HttpMethod.PUT,
new HttpEntityBuilder()
.header("Authorization", "Bearer " + token.getAccessToken())
.body(body)
.build(),
String.class
);

return List.of(
DynamicTest.dynamicTest("닉네임 누락", () -> {
var res = restExchange.apply(Map.of());
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}),
DynamicTest.dynamicTest("닉네임 길이 제한 초과", () -> {
var res = restExchange.apply(Map.of(
"nickname", "12345678901"
));
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
})
);
}
}
Loading
Loading