diff --git a/src/main/java/in/koreatech/koin/domain/dept/controller/DeptController.java b/src/main/java/in/koreatech/koin/domain/dept/controller/DeptController.java index 24bc3e7107..7ad46ed198 100644 --- a/src/main/java/in/koreatech/koin/domain/dept/controller/DeptController.java +++ b/src/main/java/in/koreatech/koin/domain/dept/controller/DeptController.java @@ -19,7 +19,9 @@ public class DeptController implements DeptApi { private final DeptService deptService; @GetMapping("/dept") - public ResponseEntity getDept(@RequestParam(value = "dept_num") String deptNumber) { + public ResponseEntity getDept( + @RequestParam(value = "dept_num") String deptNumber + ) { DeptResponse foundDepartment = deptService.getById(deptNumber); if (foundDepartment == null) { return ResponseEntity.ok().build(); diff --git a/src/main/java/in/koreatech/koin/domain/land/controller/LandController.java b/src/main/java/in/koreatech/koin/domain/land/controller/LandController.java index ba1a3252ee..575429527d 100644 --- a/src/main/java/in/koreatech/koin/domain/land/controller/LandController.java +++ b/src/main/java/in/koreatech/koin/domain/land/controller/LandController.java @@ -25,7 +25,9 @@ public ResponseEntity> getLands() { } @GetMapping("/lands/{id}") - public ResponseEntity getLand(@PathVariable Long id) { + public ResponseEntity getLand( + @PathVariable Long id + ) { LandResponse response = landService.getLand(id); return ResponseEntity.ok(response); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java index 56d0d1a69b..3b8635883e 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java @@ -17,13 +17,18 @@ public class ShopController implements ShopApi { private final ShopService shopService; @GetMapping("/shops/{shopId}/menus/{menuId}") - public ResponseEntity findMenu(@PathVariable Long shopId, @PathVariable Long menuId) { + public ResponseEntity findMenu( + @PathVariable Long shopId, + @PathVariable Long menuId + ) { ShopMenuResponse shopMenu = shopService.findMenu(menuId); return ResponseEntity.ok(shopMenu); } @GetMapping("/shops/{shopId}/menus/categories") - public ResponseEntity findMenuCategories(@PathVariable Long shopId) { + public ResponseEntity findMenuCategories( + @PathVariable Long shopId + ) { MenuCategoriesResponse menuCategories = shopService.getMenuCategories(shopId); return ResponseEntity.ok(menuCategories); } diff --git a/src/main/java/in/koreatech/koin/domain/track/controller/TrackController.java b/src/main/java/in/koreatech/koin/domain/track/controller/TrackController.java index b91f78ce0d..c1d705f392 100644 --- a/src/main/java/in/koreatech/koin/domain/track/controller/TrackController.java +++ b/src/main/java/in/koreatech/koin/domain/track/controller/TrackController.java @@ -25,7 +25,9 @@ public ResponseEntity> getTracks() { } @GetMapping("/tracks/{id}") - public ResponseEntity getTrack(@PathVariable Long id) { + public ResponseEntity getTrack( + @PathVariable Long id + ) { TrackSingleResponse response = trackService.getTrack(id); return ResponseEntity.ok(response); } diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java index 9805f0c6c8..7a597b3b9a 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java @@ -1,6 +1,7 @@ package in.koreatech.koin.domain.user.controller; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -81,4 +82,19 @@ ResponseEntity logout( ResponseEntity refresh( @RequestBody @Valid UserTokenRefreshRequest request ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "회원 탈퇴") + @SecurityRequirement(name = "Jwt Authentication") + @DeleteMapping("/user") + ResponseEntity withdraw( + @Auth(permit = {STUDENT}) Long userId + ); } diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index a1a14bf831..32bc837920 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -3,6 +3,7 @@ import java.net.URI; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -28,20 +29,25 @@ public class UserController implements UserApi { private final StudentService studentService; @GetMapping("/user/student/me") - public ResponseEntity getStudent(@Auth(permit = STUDENT) Long userId) { + public ResponseEntity getStudent( + @Auth(permit = STUDENT) Long userId + ) { StudentResponse studentResponse = studentService.getStudent(userId); return ResponseEntity.ok().body(studentResponse); } @PostMapping("/user/login") - public ResponseEntity login(@RequestBody @Valid UserLoginRequest request) { + public ResponseEntity login( + @RequestBody @Valid UserLoginRequest request + ) { UserLoginResponse response = userService.login(request); return ResponseEntity.created(URI.create("/")) .body(response); } @PostMapping("/user/logout") - public ResponseEntity logout(@Auth(permit = {STUDENT}) Long userId) { + public ResponseEntity logout( + @Auth(permit = {STUDENT}) Long userId) { userService.logout(userId); return ResponseEntity.ok().build(); } @@ -53,4 +59,12 @@ public ResponseEntity refresh( UserTokenRefreshResponse tokenGroupResponse = userService.refresh(request); return ResponseEntity.ok().body(tokenGroupResponse); } + + @DeleteMapping("/user") + public ResponseEntity withdraw( + @Auth(permit = {STUDENT}) Long userId + ) { + userService.withdraw(userId); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/model/User.java b/src/main/java/in/koreatech/koin/domain/user/model/User.java index 085196bb6b..b3b1d0ff46 100644 --- a/src/main/java/in/koreatech/koin/domain/user/model/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/model/User.java @@ -2,6 +2,9 @@ import java.time.LocalDateTime; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + import in.koreatech.koin.global.common.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -22,6 +25,8 @@ @Getter @Entity @Table(name = "users") +@Where(clause = "is_deleted=0") +@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends BaseEntity { diff --git a/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java b/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java index cce01f1f11..9f0f7865da 100644 --- a/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java +++ b/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java @@ -24,4 +24,6 @@ default User getById(Long userId) { return findById(userId) .orElseThrow(() -> UserNotFoundException.withDetail("userId: " + userId)); } + + void delete(User user); } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java index 27fae99a37..4d7ca01158 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java @@ -7,8 +7,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import in.koreatech.koin.global.auth.JwtProvider; -import in.koreatech.koin.global.auth.exception.AuthException; import in.koreatech.koin.domain.user.dto.UserLoginRequest; import in.koreatech.koin.domain.user.dto.UserLoginResponse; import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest; @@ -17,6 +15,8 @@ import in.koreatech.koin.domain.user.model.UserToken; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.domain.user.repository.UserTokenRepository; +import in.koreatech.koin.global.auth.JwtProvider; +import in.koreatech.koin.global.auth.exception.AuthException; import lombok.RequiredArgsConstructor; @Service @@ -62,11 +62,17 @@ public UserTokenRefreshResponse refresh(UserTokenRefreshRequest request) { return UserTokenRefreshResponse.of(accessToken, userToken.getRefreshToken()); } - private static String getUserId(String refreshToken) { + private String getUserId(String refreshToken) { String[] split = refreshToken.split("-"); if (split.length == 0) { throw new AuthException("올바르지 않은 인증 토큰입니다. refreshToken: " + refreshToken); } return split[split.length - 1]; } + + @Transactional + public void withdraw(Long userId) { + User user = userRepository.getById(userId); + userRepository.delete(user); + } } diff --git a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java index 2c94fe8007..b2baf86e61 100644 --- a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java @@ -47,7 +47,6 @@ void userLoginSuccess() { ExtractableResponse response = RestAssured .given() - .log().all() .body(""" { "email": "test@koreatech.ac.kr", @@ -56,10 +55,8 @@ void userLoginSuccess() { """) .contentType(ContentType.JSON) .when() - .log().all() .post("/user/login") .then() - .log().all() .statusCode(HttpStatus.CREATED.value()) .extract(); @@ -96,7 +93,6 @@ void userLogoutSuccess() { ExtractableResponse response = RestAssured .given() - .log().all() .body(""" { "email": "test@koreatech.ac.kr", @@ -105,22 +101,17 @@ void userLogoutSuccess() { """) .contentType(ContentType.JSON) .when() - .log().all() .post("/user/login") .then() - .log().all() .statusCode(HttpStatus.CREATED.value()) .extract(); RestAssured .given() - .log().all() .header("Authorization", "Bearer " + response.jsonPath().getString("token")) .when() - .log().all() .post("/user/logout") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -147,7 +138,6 @@ void userRefreshToken() { ExtractableResponse response = RestAssured .given() - .log().all() .body(""" { "email": "test@koreatech.ac.kr", @@ -156,25 +146,20 @@ void userRefreshToken() { """) .contentType(ContentType.JSON) .when() - .log().all() .post("/user/login") .then() - .log().all() .statusCode(HttpStatus.CREATED.value()) .extract(); RestAssured .given() - .log().all() .body( Map.of("refresh_token", response.jsonPath().getString("refresh_token")) ) .contentType(ContentType.JSON) .when() - .log().all() .post("/user/refresh") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); diff --git a/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java b/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java index 059d08019d..dcf211b60b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java @@ -139,12 +139,9 @@ void getArticle() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/articles/{articleId}", article1.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -214,13 +211,10 @@ void getArticleAuthorizationComment() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .header("Authorization", "Bearer " + token) .when() - .log().all() .get("/articles/{articleId}", article1.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -243,15 +237,12 @@ void getArticlesByPagination() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER) .param("limit", PAGE_LIMIT) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -314,15 +305,12 @@ void getArticlesByPagination_0Page() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER_ZERO) .param("limit", PAGE_LIMIT) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -342,15 +330,12 @@ void getArticlesByPagination_lessThan0Pages() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER_MINUS) .param("limit", PAGE_LIMIT) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -370,15 +355,12 @@ void getArticlesByPagination_1Limit() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER) .param("limit", PAGE_LIMIT_ZERO) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -398,15 +380,12 @@ void getArticlesByPagination_lessThan0Limit() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER) .param("limit", PAGE_LIMIT_ZERO) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -447,15 +426,12 @@ void getArticlesByPagination_over50Limit() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER) .param("limit", PAGE_LIMIT_ZERO) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -497,13 +473,10 @@ void getArticlesByPagination_default() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -525,15 +498,12 @@ void getArticlesByPagination_overMaxPageNotFound() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("boardId", board.getId()) .param("page", PAGE_NUMBER) .param("limit", PAGE_LIMIT) .get("/articles") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); diff --git a/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java b/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java index f57dd35fb3..2adbdf1276 100644 --- a/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java @@ -1,7 +1,5 @@ package in.koreatech.koin.acceptance; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -11,6 +9,7 @@ import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import static org.assertj.core.api.SoftAssertions.assertSoftly; class DeptApiTest extends AcceptanceTest { @@ -23,13 +22,10 @@ void findDeptNameByDeptNumber() { // when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .param("dept_num", dept.getNumbers().get(0)) .get("/dept") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -50,12 +46,9 @@ void findAllDepts() { //when then ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/depts") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -65,8 +58,10 @@ void findAllDepts() { .isEqualTo(DEPT_SIZE); for (int i = 0; i < DEPT_SIZE; i++) { softly.assertThat(response.body().jsonPath().getString(String.format("[%d].name", i))).isNotEmpty(); - softly.assertThat(response.body().jsonPath().getString(String.format("[%d].curriculum_link", i))).isNotEmpty(); - softly.assertThat(response.body().jsonPath().getString(String.format("[%d].dept_nums[0]", i))).isNotEmpty(); + softly.assertThat(response.body().jsonPath().getString(String.format("[%d].curriculum_link", i))) + .isNotEmpty(); + softly.assertThat(response.body().jsonPath().getString(String.format("[%d].dept_nums[0]", i))) + .isNotEmpty(); } } ); diff --git a/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java b/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java index c925efde5e..1a796aa2b7 100644 --- a/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java @@ -39,12 +39,9 @@ void getLands() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/lands") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -96,12 +93,9 @@ void getLand() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/lands/{id}", land.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); diff --git a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java index 9c46310c54..ab881e4680 100644 --- a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java @@ -62,12 +62,9 @@ void findMenuSingleOption() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -142,12 +139,9 @@ void findMenuMultipleOption() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -219,12 +213,9 @@ void findShopMenuCategories() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/shops/{shopId}/menus/categories", menu.getShopId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); diff --git a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java index 88aa2a3758..1659a78e9d 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java @@ -38,12 +38,9 @@ void findTracks() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/tracks") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -91,12 +88,9 @@ void findTrack() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/tracks/{id}", track.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -153,12 +147,9 @@ void findTrackWithEmptyMembersAndTechStacks() { ExtractableResponse response = RestAssured .given() - .log().all() .when() - .log().all() .get("/tracks/{id}", track.getId()) .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index 19a39cd955..a302df4e3a 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -1,5 +1,6 @@ package in.koreatech.koin.acceptance; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -59,13 +60,10 @@ void studentCheckMe() { ExtractableResponse response = RestAssured .given() - .log().all() .header("Authorization", "Bearer " + token) .when() - .log().all() .get("/user/student/me") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -122,13 +120,10 @@ void studentCheckMeUnAuthorized() { ExtractableResponse response = RestAssured .given() - .log().all() .header("Authorization", "Bearer " + token) .when() - .log().all() .get("/user/student/me") .then() - .log().all() .statusCode(HttpStatus.UNAUTHORIZED.value()) .extract(); } @@ -153,14 +148,41 @@ void studentCheckMeNotFound() { ExtractableResponse response = RestAssured .given() - .log().all() .header("Authorization", "Bearer " + token) .when() - .log().all() .get("/user/student/me") .then() - .log().all() .statusCode(HttpStatus.NOT_FOUND.value()) .extract(); } + + @Test + @DisplayName("회원이 탈퇴한다") + void userWithdraw() { + User user = User.builder() + .password("1234") + .nickname("주노") + .name("최준호") + .phoneNumber("010-1234-5678") + .userType(STUDENT) + .gender(UserGender.MAN) + .email("test@koreatech.ac.kr") + .isAuthed(true) + .isDeleted(false) + .build(); + + userRepository.save(user); + String token = jwtProvider.createToken(user); + + RestAssured + .given() + .header("Authorization", "Bearer " + token) + .when() + .delete("/user") + .then() + .statusCode(HttpStatus.NO_CONTENT.value()) + .extract(); + + Assertions.assertThat(userRepository.findById(user.getId())).isNotPresent(); + } }