diff --git a/src/main/java/com/soptie/server/auth/controller/AuthController.java b/src/main/java/com/soptie/server/auth/controller/AuthController.java index 7b643e68..10273d8e 100644 --- a/src/main/java/com/soptie/server/auth/controller/AuthController.java +++ b/src/main/java/com/soptie/server/auth/controller/AuthController.java @@ -1,7 +1,6 @@ package com.soptie.server.auth.controller; import com.soptie.server.auth.dto.SignInRequest; -import com.soptie.server.auth.message.ResponseMessage; import com.soptie.server.auth.service.AuthService; import com.soptie.server.common.dto.Response; import lombok.RequiredArgsConstructor; @@ -9,6 +8,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.security.Principal; + +import static com.soptie.server.auth.message.ResponseMessage.*; import static com.soptie.server.common.dto.Response.success; @RestController @@ -21,6 +23,13 @@ public class AuthController { @PostMapping public ResponseEntity signIn(@RequestHeader("Authorization") String socialAccessToken, @RequestBody SignInRequest request) { val response = authService.signIn(socialAccessToken, request); - return ResponseEntity.ok(success(ResponseMessage.SUCCESS_SIGNIN.getMessage(), response)); + return ResponseEntity.ok(success(SUCCESS_SIGN_IN.getMessage(), response)); + } + + @PostMapping("/logout") + public ResponseEntity signOut(Principal principal) { + val memberId = Long.parseLong(principal.getName()); + authService.signOut(memberId); + return ResponseEntity.ok(success(SUCCESS_SIGN_OUT.getMessage(), null)); } } diff --git a/src/main/java/com/soptie/server/auth/message/ResponseMessage.java b/src/main/java/com/soptie/server/auth/message/ResponseMessage.java index 43b63fd7..d7020baa 100644 --- a/src/main/java/com/soptie/server/auth/message/ResponseMessage.java +++ b/src/main/java/com/soptie/server/auth/message/ResponseMessage.java @@ -7,7 +7,9 @@ @Getter public enum ResponseMessage { - SUCCESS_SIGNIN("소셜로그인 성공"); + SUCCESS_SIGN_IN("소셜로그인 성공"), + SUCCESS_SIGN_OUT("로그아웃 성공"), + ; private final String message; } diff --git a/src/main/java/com/soptie/server/auth/service/AuthService.java b/src/main/java/com/soptie/server/auth/service/AuthService.java index c9f3165a..338f5b42 100644 --- a/src/main/java/com/soptie/server/auth/service/AuthService.java +++ b/src/main/java/com/soptie/server/auth/service/AuthService.java @@ -6,4 +6,5 @@ public interface AuthService { SignInResponse signIn(String socialAccessToken, SignInRequest request); + void signOut(Long memberId); } diff --git a/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java b/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java index 0810cfb7..2c25df60 100644 --- a/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java @@ -9,6 +9,7 @@ import com.soptie.server.member.entity.Member; import com.soptie.server.member.entity.SocialType; import com.soptie.server.member.repository.MemberRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.val; import org.springframework.security.core.Authentication; @@ -16,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional; import static com.soptie.server.auth.message.ErrorMessage.INVALID_TOKEN; +import static com.soptie.server.member.message.ErrorMessage.INVALID_MEMBER; @Service @RequiredArgsConstructor @@ -33,6 +35,13 @@ public SignInResponse signIn(String socialAccessToken, SignInRequest request) { return SignInResponse.of(getToken(getMember(socialAccessToken, request))); } + @Override + @Transactional + public void signOut(Long memberId) { + val member = findMember(memberId); + member.resetRefreshToken(); + } + private Member getMember(String socialAccessToken, SignInRequest request) { val socialType = request.socialType(); val socialId = getSocialId(socialAccessToken, socialType); @@ -71,4 +80,9 @@ private Token generateToken(Authentication authentication) { .refreshToken(jwtTokenProvider.generateToken(authentication, valueConfig.getRefreshTokenExpired())) .build(); } + + private Member findMember(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(INVALID_MEMBER.getMeesage())); + } } diff --git a/src/main/java/com/soptie/server/member/controller/MemberController.java b/src/main/java/com/soptie/server/member/controller/MemberController.java index 633df4eb..50e17e79 100644 --- a/src/main/java/com/soptie/server/member/controller/MemberController.java +++ b/src/main/java/com/soptie/server/member/controller/MemberController.java @@ -55,6 +55,5 @@ public ResponseEntity getMemberHomeInfo(Principal principal) { val memberId = Long.parseLong(principal.getName()); val memberHomeInfoResponse = memberService.getMemberHomeInfo(memberId); return ResponseEntity.ok(success(SUCCESS_HOME_INFO.getMessage(), memberHomeInfoResponse)); - } } diff --git a/src/main/java/com/soptie/server/member/entity/Member.java b/src/main/java/com/soptie/server/member/entity/Member.java index 79806c5c..58a85254 100644 --- a/src/main/java/com/soptie/server/member/entity/Member.java +++ b/src/main/java/com/soptie/server/member/entity/Member.java @@ -76,6 +76,10 @@ public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } + public void resetRefreshToken() { + this.refreshToken = null; + } + public void addDailyCotton() { this.cottonInfo.addDailyCotton(); } diff --git a/src/main/resources/static/docs/open-api-3.0.1.json b/src/main/resources/static/docs/open-api-3.0.1.json index d69820f3..fe163f91 100644 --- a/src/main/resources/static/docs/open-api-3.0.1.json +++ b/src/main/resources/static/docs/open-api-3.0.1.json @@ -71,11 +71,11 @@ "content" : { "application/json;charset=UTF-8" : { "schema" : { - "$ref" : "#/components/schemas/api-v1-members935091773" + "$ref" : "#/components/schemas/api-v1-members-980081866" }, "examples" : { "get-member-home-screen-docs" : { - "value" : "{\r\n \"success\" : true,\r\n \"message\" : \"홈 화면 불러오기 성공\",\r\n \"data\" : {\r\n \"name\" : \"softie\",\r\n \"dollType\" : \"BROWN\",\r\n \"attentionImageUrl\" : \"attentionImageUrl\",\r\n \"frameImageUrl\" : \"frameImageUrl\",\r\n \"dailyCottonCount\" : 0,\r\n \"happinessCottonCount\" : 0,\r\n \"conversations\" : [ \"안녕\", \"하이\", \"봉쥬르\" ]\r\n }\r\n}" + "value" : "{\r\n \"success\" : true,\r\n \"message\" : \"홈 화면 불러오기 성공\",\r\n \"data\" : {\r\n \"name\" : \"softie\",\r\n \"dollType\" : \"BROWN\",\r\n \"frameImageUrl\" : \"frameImageUrl\",\r\n \"dailyCottonCount\" : 0,\r\n \"happinessCottonCount\" : 0,\r\n \"conversations\" : [ \"안녕\", \"하이\", \"봉쥬르\" ]\r\n }\r\n}" } } } @@ -154,6 +154,31 @@ } } }, + "/api/v1/auth/logout" : { + "post" : { + "tags" : [ "AUTH" ], + "summary" : "로그아웃", + "description" : "로그아웃", + "operationId" : "post-logout-docs", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v1-members594740350" + }, + "examples" : { + "post-logout-docs" : { + "value" : "{\r\n \"success\" : true,\r\n \"message\" : \"로그아웃 성공\",\r\n \"data\" : null\r\n}" + } + } + } + } + } + } + } + }, "/api/v1/members/{cottonType}" : { "patch" : { "tags" : [ "MEMBER" ], @@ -479,7 +504,7 @@ }, "components" : { "schemas" : { - "api-v1-members935091773" : { + "api-v1-members-980081866" : { "type" : "object", "properties" : { "data" : { @@ -493,10 +518,6 @@ "type" : "string", "description" : "인형 이름" }, - "attentionImageUrl" : { - "type" : "string", - "description" : "인형 이미지 url" - }, "frameImageUrl" : { "type" : "string", "description" : "인형 배경 이미지 url" diff --git a/src/test/java/com/soptie/server/auth/controller/AuthControllerTest.java b/src/test/java/com/soptie/server/auth/controller/AuthControllerTest.java index 248c8dfa..dc584318 100644 --- a/src/test/java/com/soptie/server/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/soptie/server/auth/controller/AuthControllerTest.java @@ -16,9 +16,14 @@ import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import java.security.Principal; + import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.soptie.server.auth.message.ResponseMessage.SUCCESS_SIGN_IN; +import static com.soptie.server.auth.message.ResponseMessage.SUCCESS_SIGN_OUT; import static org.mockito.Mockito.when; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.JsonFieldType.*; @@ -31,6 +36,8 @@ class AuthControllerTest extends BaseControllerTest { @MockBean AuthController controller; + @MockBean + Principal principal; private final String DEFAULT_URL = "/api/v1/auth"; private final String TAG = "AUTH"; @@ -47,7 +54,7 @@ void success_getTokenBySocialAccessToken() throws Exception { .refreshToken("token") .build() ); - ResponseEntity result = ResponseEntity.ok(Response.success("소셜로그인 성공", response)); + ResponseEntity result = ResponseEntity.ok(Response.success(SUCCESS_SIGN_IN.getMessage(), response)); // when when(controller.signIn(socialAccessToken, request)).thenReturn(result); @@ -55,7 +62,7 @@ void success_getTokenBySocialAccessToken() throws Exception { // then mockMvc .perform( - RestDocumentationRequestBuilders.post(DEFAULT_URL) + post(DEFAULT_URL) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .header("Authorization", socialAccessToken) @@ -84,4 +91,39 @@ void success_getTokenBySocialAccessToken() throws Exception { .build()))) .andExpect(status().isOk()); } + + @Test + @DisplayName("로그아웃 성공") + void success_signOut() throws Exception { + // given + ResponseEntity result = ResponseEntity.ok(Response.success(SUCCESS_SIGN_OUT.getMessage(), null)); + + // when + when(controller.signOut(principal)).thenReturn(result); + + // then + mockMvc + .perform( + post(DEFAULT_URL + "/logout") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .principal(principal) + ) + .andDo( + MockMvcRestDocumentation.document( + "post-logout-docs", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag(TAG) + .description("로그아웃") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build()))) + .andExpect(status().isOk()); + } }