From 2463024b5b5151e716784a6ca6945694367dbbdc Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 19:19:56 +0900 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20swagger=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20API=20=EB=B0=8F=20DTO=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../rabbitmqprac/application/api/AuthApi.java | 27 +++++++ .../application/api/ChatMessageApi.java | 81 +++++++++++++++++++ .../application/api/ChatRoomApi.java | 33 ++++++++ .../application/api/ChatRoomMemberApi.java | 23 ++++++ .../application/api/OauthApi.java | 42 ++++++++++ .../rabbitmqprac/application/api/UserApi.java | 39 +++++++++ .../controller/AuthController.java | 12 ++- .../controller/ChatMessageController.java | 6 +- .../controller/ChatRoomController.java | 6 +- .../controller/ChatRoomMemberController.java | 3 +- .../controller/OauthController.java | 9 ++- .../controller/UserController.java | 8 +- .../dto/auth/req/AuthSignInReq.java | 4 + .../dto/auth/req/AuthSignUpReq.java | 6 ++ .../dto/auth/req/AuthUpdatePasswordReq.java | 10 ++- .../dto/auth/res/UserDetailRes.java | 5 ++ .../dto/chatmessage/req/ChatMessageReq.java | 5 +- .../chatmessage/res/ChatMessageDetailRes.java | 8 ++ .../dto/chatmessage/res/ChatMessageRes.java | 7 ++ .../res/LastChatMessageDetailRes.java | 6 ++ .../dto/chatroom/req/ChatRoomCreateReq.java | 4 + .../dto/chatroom/res/ChatRoomDetailRes.java | 8 ++ .../dto/chatroom/res/ChatRoomInfoRes.java | 8 ++ .../res/ChatRoomMemberDetailRes.java | 5 ++ .../dto/oauth/req/OauthSignInReq.java | 5 +- .../dto/oauth/req/OauthSignUpReq.java | 6 +- .../dto/user/req/NicknameCheckReq.java | 3 + .../dto/user/req/NicknameUpdateReq.java | 3 + .../rabbitmqprac/config/SwaggerConfig.java | 66 +++++++++++++++ .../exception/payload/ErrorResponse.java | 9 +++ src/main/resources/application.yml | 5 ++ 32 files changed, 448 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/rabbitmqprac/application/api/AuthApi.java create mode 100644 src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java create mode 100644 src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java create mode 100644 src/main/java/com/rabbitmqprac/application/api/ChatRoomMemberApi.java create mode 100644 src/main/java/com/rabbitmqprac/application/api/OauthApi.java create mode 100644 src/main/java/com/rabbitmqprac/application/api/UserApi.java create mode 100644 src/main/java/com/rabbitmqprac/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 13ecd79..50850dc 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,9 @@ dependencies { // WebClient implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' } tasks.named('test') { diff --git a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java new file mode 100644 index 0000000..482ec3a --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java @@ -0,0 +1,27 @@ +package com.rabbitmqprac.application.api; + +import com.rabbitmqprac.application.dto.auth.req.AuthSignInReq; +import com.rabbitmqprac.application.dto.auth.req.AuthSignUpReq; +import com.rabbitmqprac.application.dto.auth.req.AuthUpdatePasswordReq; +import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.Map; + +@Tag(name = "[인증 API]") +public interface AuthApi { + + @Operation(summary = "일반 회원가입") + ResponseEntity> signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq); + + @Operation(summary = "일반 로그인") + ResponseEntity> signIn(@RequestBody @Validated AuthSignInReq authSignInReq); + + @Operation(summary = "비밀번호 변경") + ResponseEntity patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq); +} diff --git a/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java b/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java new file mode 100644 index 0000000..59312aa --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java @@ -0,0 +1,81 @@ +package com.rabbitmqprac.application.api; + +import com.rabbitmqprac.application.dto.chatmessage.req.ChatMessageReq; +import com.rabbitmqprac.application.dto.chatmessage.res.ChatMessageDetailRes; +import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; +import com.rabbitmqprac.infra.security.principal.UserPrincipal; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Tag(name = "[채팅 메시지 API]") +public interface ChatMessageApi { + + @RequestMapping(method = RequestMethod.OPTIONS) + @Operation( + summary = "채팅 메시지 전송 (STOMP 설명용)", + description = """ + STOMP를 통해 채팅 메시지를 전송하는 방법을 설명합니다. ※ 이 API는 HTTP가 아닌 WebSocket(STOMP) 기반으로 실제로 동작하지 않습니다. + - STOMP Destination: /pub/chat.room.{chatRoomId}/message + - Payload: ChatMessageReq {"content": "메시지 내용"} + - 인증: Header에 JWT 토큰 필요 + """ + ) + void sendMessage(UserPrincipal principal, + @DestinationVariable Long chatRoomId, + @Validated ChatMessageReq message + ); + + @Operation(summary = "채팅 메시지 목록 조회 (이전)", description = "특정 채팅 메시지 ID 이전의 채팅 메시지 목록을 조회한다.") + @Parameters({ + @Parameter( + name = "lastChatMessageId", + description = """ + 조회 시작 기준이 되는 채팅 메시지 ID. 이 ID 이전의 메시지들을 조회한다. + 기본값은 0이며, 이 경우 가장 최근 메시지부터 조회를 시작한다. + """, + example = "0" + ), + @Parameter( + name = "size", + description = "한 번에 조회할 채팅 메시지의 최대 개수. 기본값은 30이다.", + example = "30" + ) + }) + List readChatMessagesBefore( + @PathVariable Long chatRoomId, + @RequestParam(value = "lastChatMessageId", defaultValue = "0") Long lastMessageId, + @RequestParam(value = "size", defaultValue = "30") int size + ); + + @Operation(summary = "채팅 메시지 목록 조회 (범위)") + @Parameters({ + @Parameter( + name = "from", + description = "조회할 채팅 메시지 ID 범위의 시작. 이 ID를 포함한다.", + example = "100", + required = true + ), + @Parameter( + name = "to", + description = "조회할 채팅 메시지 ID 범위의 끝. 이 ID를 포함한다.", + example = "200", + required = true + ) + }) + List readChatMessagesBetween(@AuthenticationPrincipal SecurityUserDetails user, + @PathVariable Long chatRoomId, + @RequestParam Long from, + @RequestParam Long to + ); +} diff --git a/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java b/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java new file mode 100644 index 0000000..c1439ca --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java @@ -0,0 +1,33 @@ +package com.rabbitmqprac.application.api; + +import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Tag(name = "[채팅방 API]") +public interface ChatRoomApi { + + @Operation(summary = "채팅방 생성") + ChatRoomDetailRes create( + @AuthenticationPrincipal SecurityUserDetails user, + @RequestBody @Validated ChatRoomCreateReq chatRoomCreateReq + ); + + @Operation(summary = "가입한 채팅방 목록 조회", description = "로그인된 유저의 채팅방 목록을 조회한다.") + List getMyChatRooms(@AuthenticationPrincipal SecurityUserDetails user); + + @Operation(summary = "채팅방 목록 조회") + List getChatRooms(@AuthenticationPrincipal SecurityUserDetails user); +} diff --git a/src/main/java/com/rabbitmqprac/application/api/ChatRoomMemberApi.java b/src/main/java/com/rabbitmqprac/application/api/ChatRoomMemberApi.java new file mode 100644 index 0000000..54513ce --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/api/ChatRoomMemberApi.java @@ -0,0 +1,23 @@ +package com.rabbitmqprac.application.api; + +import com.rabbitmqprac.application.dto.chatroommember.res.ChatRoomMemberDetailRes; +import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@Tag(name = "[채팅방 멤버 API]") +public interface ChatRoomMemberApi { + + @Operation(summary = "채팅방 참여") + void joinChatRoom( + @AuthenticationPrincipal SecurityUserDetails user, + @PathVariable Long chatRoomId + ); + + @Operation(summary = "채팅방 멤버 목록 조회") + List getChatRoomMembers(@PathVariable Long chatRoomId); +} diff --git a/src/main/java/com/rabbitmqprac/application/api/OauthApi.java b/src/main/java/com/rabbitmqprac/application/api/OauthApi.java new file mode 100644 index 0000000..81085a2 --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/api/OauthApi.java @@ -0,0 +1,42 @@ +package com.rabbitmqprac.application.api; + +import com.rabbitmqprac.application.dto.oauth.req.OauthSignInReq; +import com.rabbitmqprac.application.dto.oauth.req.OauthSignUpReq; +import com.rabbitmqprac.domain.persistence.oauth.constant.OauthProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "[소셜 인증 API]") +public interface OauthApi { + + @Operation( + summary = "소셜 로그인", + description = """ + code는 각 소셜 플랫폼에서 발급받은 인가 코드로 KAKAO, GOOGLE 등의 OauthProvider를 통해 발급받은 인가 코드를 의미합니다. + - [KAKAO CODE 발급 URL](https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=57e328d97dc0c7b53898e2e79082f23c&redirect_uri=http://localhost:8080&scope=openid%20profile_nickname&nonce=example-nonce) + - [GOOGLE CODE 발급 URL](https://accounts.google.com/o/oauth2/v2/auth?client_id=248388975343-0oo0f79rrsqpf1k63ahpivkhd2rfu1jp.apps.googleusercontent.com&redirect_uri=http://localhost:8080&response_type=code&scope=openid%20email%20profile&nonce=example-nonce) + """ + ) + ResponseEntity signIn( + @RequestParam OauthProvider oauthProvider, + @RequestBody @Validated OauthSignInReq req + ); + + + @Operation( + summary = "소셜 회원가입", + description = """ + code는 각 소셜 플랫폼에서 발급받은 인가 코드로 KAKAO, GOOGLE 등의 OauthProvider를 통해 발급받은 인가 코드를 의미합니다. + - [KAKAO CODE 발급 URL](https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=57e328d97dc0c7b53898e2e79082f23c&redirect_uri=http://localhost:8080&scope=openid%20profile_nickname&nonce=example-nonce) + - [GOOGLE CODE 발급 URL](https://accounts.google.com/o/oauth2/v2/auth?client_id=248388975343-0oo0f79rrsqpf1k63ahpivkhd2rfu1jp.apps.googleusercontent.com&redirect_uri=http://localhost:8080&response_type=code&scope=openid%20email%20profile&nonce=example-nonce) + """ + ) + ResponseEntity signUp( + @RequestParam OauthProvider oauthProvider, + @RequestBody @Validated OauthSignUpReq req + ); +} diff --git a/src/main/java/com/rabbitmqprac/application/api/UserApi.java b/src/main/java/com/rabbitmqprac/application/api/UserApi.java new file mode 100644 index 0000000..9690533 --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/api/UserApi.java @@ -0,0 +1,39 @@ +package com.rabbitmqprac.application.api; + +import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; +import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; +import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; +import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; +import java.util.Map; + +@Tag(name = "[유저 API]") +public interface UserApi { + + @Operation(summary = "내 정보 조회") + UserDetailRes getMember(@AuthenticationPrincipal SecurityUserDetails user); + + @Operation(summary = "전체 회원 조회") + List getMembers(); + + @Operation(summary = "유저 아이디 중복 확인") + Map isDuplicatedUsername(@RequestParam @Validated String username); + + @Operation(summary = "닉네임 변경") + ResponseEntity patchNickname( + @AuthenticationPrincipal SecurityUserDetails user, + @RequestBody NicknameUpdateReq nicknameUpdateReq + + ); + + @Operation(summary = "닉네임 중복 확인") + Map checkNicknameDuplication(@Validated NicknameCheckReq nicknameCheckReq); +} diff --git a/src/main/java/com/rabbitmqprac/application/controller/AuthController.java b/src/main/java/com/rabbitmqprac/application/controller/AuthController.java index f77a0ac..262aa03 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/AuthController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/AuthController.java @@ -1,5 +1,6 @@ package com.rabbitmqprac.application.controller; +import com.rabbitmqprac.application.api.AuthApi; import com.rabbitmqprac.application.dto.auth.req.AuthSignInReq; import com.rabbitmqprac.application.dto.auth.req.AuthSignUpReq; import com.rabbitmqprac.application.dto.auth.req.AuthUpdatePasswordReq; @@ -28,26 +29,29 @@ @RequiredArgsConstructor @RequestMapping("/auth") @RestController -public class AuthController { +public class AuthController implements AuthApi { private final AuthService authService; + @Override @PostMapping("/sign-up") - public ResponseEntity signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq) { + public ResponseEntity> signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq) { return createAuthenticatedResponse(authService.signUp(authSignUpReq)); } + @Override @PostMapping("/sign-in") - public ResponseEntity signIn(@RequestBody @Validated AuthSignInReq authSignInReq) { + public ResponseEntity> signIn(@RequestBody @Validated AuthSignInReq authSignInReq) { return createAuthenticatedResponse(authService.signIn(authSignInReq)); } + @Override @PatchMapping("/password") public ResponseEntity patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq) { authService.updatePassword(user.getUserId(), authUpdatePasswordReq); return ResponseEntity.noContent().build(); } - private ResponseEntity createAuthenticatedResponse(Pair userInfo) { + private ResponseEntity> createAuthenticatedResponse(Pair userInfo) { ResponseCookie cookie = CookieUtil.createCookie( "refreshToken", userInfo.getValue().refreshToken(), Duration.ofDays(7).toSeconds() ); diff --git a/src/main/java/com/rabbitmqprac/application/controller/ChatMessageController.java b/src/main/java/com/rabbitmqprac/application/controller/ChatMessageController.java index 59cdbb0..868ae78 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/ChatMessageController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/ChatMessageController.java @@ -1,5 +1,6 @@ package com.rabbitmqprac.application.controller; +import com.rabbitmqprac.application.api.ChatMessageApi; import com.rabbitmqprac.application.dto.chatmessage.req.ChatMessageReq; import com.rabbitmqprac.application.dto.chatmessage.res.ChatMessageDetailRes; import com.rabbitmqprac.domain.context.chatmessage.service.ChatMessageService; @@ -20,13 +21,14 @@ @RestController @RequiredArgsConstructor -public class ChatMessageController { +public class ChatMessageController implements ChatMessageApi { private final ChatMessageService chatMessageService; /** * Destination Queue: /pub/chat.message.{chatRoomId}를 통해 호출 후 처리 되는 로직 */ + @Override @PreAuthorize("#chatRoomAccessChecker.hasPermission(#chatRoomId, principal)") @MessageMapping("chat.room.{chatRoomId}/message") public void sendMessage(UserPrincipal principal, @@ -35,6 +37,7 @@ public void sendMessage(UserPrincipal principal, chatMessageService.sendMessage(principal.getUserId(), chatRoomId, message); } + @Override @GetMapping("/chat-rooms/{chatRoomId}/messages/before") public List readChatMessagesBefore(@PathVariable Long chatRoomId, @RequestParam(value = "lastChatMessageId", defaultValue = "0") Long lastMessageId, @@ -43,6 +46,7 @@ public List readChatMessagesBefore(@PathVariable Long chat return chatMessageService.readChatMessagesBefore(chatRoomId, lastMessageId, size); } + @Override @GetMapping("/chat-rooms/{chatRoomId}/messages/between") public List readChatMessagesBetween(@AuthenticationPrincipal SecurityUserDetails user, @PathVariable Long chatRoomId, diff --git a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java index 20a4b29..7f60054 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java @@ -1,5 +1,6 @@ package com.rabbitmqprac.application.controller; +import com.rabbitmqprac.application.api.ChatRoomApi; import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; @@ -21,21 +22,24 @@ @RestController @RequiredArgsConstructor @Slf4j -public class ChatRoomController { +public class ChatRoomController implements ChatRoomApi { private final ChatRoomService chatRoomService; + @Override @PostMapping("/chat-rooms") public ChatRoomDetailRes create(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated ChatRoomCreateReq chatRoomCreateReq) { return chatRoomService.create(user.getUserId(), chatRoomCreateReq); } + @Override @GetMapping("/chat-rooms/me") public List getMyChatRooms(@AuthenticationPrincipal SecurityUserDetails user) { return chatRoomService.getMyChatRooms(user.getUserId()); } + @Override @GetMapping("/chat-rooms") public List getChatRooms(@AuthenticationPrincipal SecurityUserDetails user) { return chatRoomService.getChatRooms(Optional.ofNullable(Objects.isNull(user) ? null : user.getUserId()) diff --git a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java index 1769997..3642373 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java @@ -1,5 +1,6 @@ package com.rabbitmqprac.application.controller; +import com.rabbitmqprac.application.api.ChatRoomMemberApi; import com.rabbitmqprac.application.dto.chatroommember.res.ChatRoomMemberDetailRes; import com.rabbitmqprac.domain.context.chatroommember.service.ChatRoomMemberService; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; @@ -14,7 +15,7 @@ @RequiredArgsConstructor @RestController -public class ChatRoomMemberController { +public class ChatRoomMemberController implements ChatRoomMemberApi { private final ChatRoomMemberService chatRoomMemberService; diff --git a/src/main/java/com/rabbitmqprac/application/controller/OauthController.java b/src/main/java/com/rabbitmqprac/application/controller/OauthController.java index 794a047..2f57264 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/OauthController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/OauthController.java @@ -1,5 +1,6 @@ package com.rabbitmqprac.application.controller; +import com.rabbitmqprac.application.api.OauthApi; import com.rabbitmqprac.application.dto.oauth.req.OauthSignInReq; import com.rabbitmqprac.application.dto.oauth.req.OauthSignUpReq; import com.rabbitmqprac.domain.context.oauth.service.OauthService; @@ -23,17 +24,19 @@ @RequiredArgsConstructor @RestController -public class OauthController { +public class OauthController implements OauthApi { private final OauthService oauthService; - @PostMapping("/oauth/sign-in") + @Override @PreAuthorize("isAnonymous()") + @PostMapping("/oauth/sign-in") public ResponseEntity signIn(@RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignInReq req) { return createAuthenticatedResponse(oauthService.signIn(oauthProvider, req)); } - @PostMapping("/oauth/sign-up") + @Override @PreAuthorize("isAnonymous()") + @PostMapping("/oauth/sign-up") public ResponseEntity signUp(@RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignUpReq req) { return createAuthenticatedResponse(oauthService.signUp(oauthProvider, req)); } diff --git a/src/main/java/com/rabbitmqprac/application/controller/UserController.java b/src/main/java/com/rabbitmqprac/application/controller/UserController.java index 0504715..a6d7f30 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/UserController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/UserController.java @@ -1,5 +1,6 @@ package com.rabbitmqprac.application.controller; +import com.rabbitmqprac.application.api.UserApi; import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; @@ -22,24 +23,28 @@ @Slf4j @RestController @RequiredArgsConstructor -public class UserController { +public class UserController implements UserApi { private final UserService userService; + @Override @GetMapping("/users/me") public UserDetailRes getMember(@AuthenticationPrincipal SecurityUserDetails user) { return userService.getUserDetail(user.getUserId()); } + @Override @GetMapping("/users") public List getMembers() { return userService.getUserDetails(); } + @Override @GetMapping("/users/username") public Map isDuplicatedUsername(@RequestParam @Validated String username) { return Map.of("isDuplicated", userService.isDuplicatedUsername(username)); } + @Override @PatchMapping("/users/nickname") public ResponseEntity patchNickname(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody NicknameUpdateReq nicknameUpdateReq) { @@ -47,6 +52,7 @@ public ResponseEntity patchNickname(@AuthenticationPrincipal SecurityUserD return ResponseEntity.noContent().build(); } + @Override @GetMapping("/users/nickname") public Map checkNicknameDuplication(@Validated NicknameCheckReq nicknameCheckReq) { return Map.of("isDuplicated", userService.isDuplicatedNickname(nicknameCheckReq)); diff --git a/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignInReq.java b/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignInReq.java index 66ad85c..f73f5ca 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignInReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignInReq.java @@ -1,13 +1,17 @@ package com.rabbitmqprac.application.dto.auth.req; import com.rabbitmqprac.global.annotation.Password; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; +@Schema(name = "authSignInReq", title = "일반 로그인 요청 DTO") public record AuthSignInReq( + @Schema(title = "아이디", example = "rabbit") @NotBlank(message = "아이디를 입력해주세요") @Pattern(regexp = "^[a-z0-9-_.]{5,20}$", message = "영문 소문자, 숫자, 특수기호 (-), (_), (.) 만 사용하여, 5~20자의 아이디를 입력해 주세요") String username, + @Schema(title = "비밀번호", example = "rabbit1234") @NotBlank(message = "비밀번호를 입력해주세요") @Password(message = "8~16자의 영문 대/소문자, 숫자, 특수문자를 사용해주세요. (적어도 하나의 영문 소문자, 숫자 포함)") String password diff --git a/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignUpReq.java b/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignUpReq.java index 8ae96b3..e79614f 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignUpReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthSignUpReq.java @@ -2,20 +2,26 @@ import com.rabbitmqprac.global.annotation.Nickname; import com.rabbitmqprac.global.annotation.Password; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import org.springframework.security.crypto.password.PasswordEncoder; +@Schema(name = "authSignUpReq", title = "일반 회원가입 요청 DTO") public record AuthSignUpReq( + @Schema(title = "닉네임", example = "스프링") @NotBlank(message = "닉네임을 입력해주세요") @Nickname String nickname, + @Schema(title = "아이디", example = "rabbit") @NotBlank(message = "아이디를 입력해주세요") @Pattern(regexp = "^[a-z0-9-_.]{5,20}$", message = "영문 소문자, 숫자만 사용하여, 5~20자의 아이디를 입력해 주세요") String username, + @Schema(title = "비밀번호", example = "rabbit1234") @NotBlank(message = "비밀번호를 입력해주세요") @Password(message = "8~16자의 영문 대/소문자, 숫자, 특수문자를 사용해주세요. (적어도 하나의 영문 소문자, 숫자 포함)") String password, + @Schema(title = "확인 비밀번호", example = "rabbit1234") @NotBlank(message = "확인 비밀번호를 입력해주세요") @Password(message = "8~16자의 영문 대/소문자, 숫자, 특수문자를 사용해주세요. (적어도 하나의 영문 소문자, 숫자 포함)") String confirmPassword diff --git a/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthUpdatePasswordReq.java b/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthUpdatePasswordReq.java index 413b495..30b2366 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthUpdatePasswordReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/auth/req/AuthUpdatePasswordReq.java @@ -1,17 +1,21 @@ package com.rabbitmqprac.application.dto.auth.req; import com.rabbitmqprac.global.annotation.Password; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import org.springframework.security.crypto.password.PasswordEncoder; +@Schema(name = "authUpdatePasswordReq", title = "비밀번호 변경 요청 DTO") public record AuthUpdatePasswordReq( + @Schema(title = "기존 비밀번호", example = "oldPassword123!") @NotBlank(message = "비밀번호를 입력해주세요") String oldPassword, + @Schema(title = "새로운 비밀번호", example = "NewPassword123!") @NotBlank(message = "새로운 비밀번호를 입력해주세요") @Password(message = "8~16자의 영문 대/소문자, 숫자, 특수문자를 사용해주세요. (적어도 하나의 영문 소문자, 숫자 포함)") String newPassword ) { - public String newPassword(PasswordEncoder bCryptPasswordEncoder) { - return bCryptPasswordEncoder.encode(newPassword); - } + public String newPassword(PasswordEncoder bCryptPasswordEncoder) { + return bCryptPasswordEncoder.encode(newPassword); + } } diff --git a/src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java b/src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java index 8cd8f00..be2112f 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java @@ -1,7 +1,12 @@ package com.rabbitmqprac.application.dto.auth.res; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "userDetailRes", title = "회원 상세정보 응답 DTO") public record UserDetailRes( + @Schema(title = "유저 ID", example = "1") Long userId, + @Schema(title = "닉네임", example = "RabbitMaster") String nickname ) { public static UserDetailRes of(Long memberId, String nickname) { diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/req/ChatMessageReq.java b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/req/ChatMessageReq.java index 2e7bf21..1d531b9 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/req/ChatMessageReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/req/ChatMessageReq.java @@ -3,11 +3,14 @@ import com.rabbitmqprac.domain.persistence.chatmessage.entity.ChatMessage; import com.rabbitmqprac.domain.persistence.chatroom.entity.ChatRoom; import com.rabbitmqprac.domain.persistence.user.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +@Schema(name = "chatMessageReq", title = "채팅 메시지 발행 DTO") public record ChatMessageReq( - @NotNull(message = "메시지 내용은 Null을 허용하지 않습니다.") + @Schema(title = "메시지 내용", example = "안녕하세요~") + @NotNull(message = "메시지 내용은 필수입니다.") @Size(min = 1, max = 1000, message = "메시지 내용은 1자 이상 1000자 이하로 입력해주세요.") String content ) { diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageDetailRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageDetailRes.java index 2354c43..a24e147 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageDetailRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageDetailRes.java @@ -3,19 +3,27 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +@Schema(name = "ChatMessageDetailRes", title = "채팅 메시지 상세 응답 DTO") public record ChatMessageDetailRes( + @Schema(title = "유저 ID", example = "1") Long userId, + @Schema(title = "닉네임", example = "RabbitMaster") String nickname, + @Schema(title = "채팅 메시지 ID", example = "1001") Long chatMessageId, + @Schema(title = "메시지 내용", example = "안녕하세요") String content, + @Schema(title = "메시지 생성 시간", example = "2023-10-05 14:30:00") @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, + @Schema(title = "읽지 않은 멤버 수", example = "3") int unreadMemberCnt ) { public static ChatMessageDetailRes of(Long userId, diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageRes.java index c443543..7f82ee8 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/ChatMessageRes.java @@ -4,16 +4,23 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.rabbitmqprac.domain.context.chatmessage.constant.MessageType; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +@Schema(name = "ChatMessageRes", title = "채팅 메시지 전송 DTO") public record ChatMessageRes( + @Schema(title = "메시지 타입", example = "CHAT_MESSAGE") MessageType messageType, + @Schema(title = "유저 ID", example = "1") Long userId, + @Schema(title = "닉네임", example = "RabbitMaster") String nickname, + @Schema(title = "메시지 내용", example = "안녕하세요") String content, + @Schema(title = "메시지 생성 시간", example = "2023-10-05 14:30:00") @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/LastChatMessageDetailRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/LastChatMessageDetailRes.java index 8f25927..88219ae 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/LastChatMessageDetailRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatmessage/res/LastChatMessageDetailRes.java @@ -3,14 +3,20 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +@Schema(name = "LastChatMessageDetailRes", title = "마지막 채팅 메시지 상세 DTO") public record LastChatMessageDetailRes( + @Schema(title = "유저 ID", example = "1") Long userId, + @Schema(title = "채팅 메시지 ID", example = "1001") Long chatMessageId, + @Schema(title = "메시지 내용", example = "안녕하세요") String content, + @Schema(title = "메시지 생성 시간", example = "2023-10-05 14:30:00") @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatroom/req/ChatRoomCreateReq.java b/src/main/java/com/rabbitmqprac/application/dto/chatroom/req/ChatRoomCreateReq.java index a3d529b..947e292 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatroom/req/ChatRoomCreateReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatroom/req/ChatRoomCreateReq.java @@ -1,13 +1,17 @@ package com.rabbitmqprac.application.dto.chatroom.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +@Schema(name = "chatRoomCreateReq", title = "채팅방 생성 요청 DTO") public record ChatRoomCreateReq( + @Schema(description = "채팅방 제목", example = "스프링 스터디") @NotBlank @Size(min = 1, max = 20) String title, + @Schema(description = "채팅방 최대 인원", example = "5") @NotNull @Size(min = 2, max = 10) Integer maxCapacity diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomDetailRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomDetailRes.java index 4d506c2..bfd360b 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomDetailRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomDetailRes.java @@ -4,12 +4,17 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.rabbitmqprac.application.dto.chatmessage.res.LastChatMessageDetailRes; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +@Schema(name = "ChatRoomDetailRes", title = "채팅방 상세 DTO") public record ChatRoomDetailRes( + @Schema(title = "채팅방 ID", example = "1") Long chatRoomId, + @Schema(title = "채팅방 제목", example = "스터디 그룹") String title, + @Schema(title = "최대 인원", example = "10") Integer maxCapacity, @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @@ -17,10 +22,13 @@ public record ChatRoomDetailRes( LastChatMessageDetailRes lastMessage, + @Schema(title = "현재 인원", example = "5") int currentCapacity, + @Schema(title = "마지막으로 읽은 메시지 ID", example = "1005") Long lastReadMessageId, + @Schema(title = "읽지 않은 메시지 수", example = "3") int unreadMessageCount ) { public static ChatRoomDetailRes of(Long chatRoomId, diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java index 29c7699..f36c37c 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java @@ -3,19 +3,27 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +@Schema(name = "ChatRoomInfoRes", title = "채팅방 정보 DTO") public record ChatRoomInfoRes( + @Schema(title = "채팅방 ID", example = "1") Long chatRoomId, + @Schema(title = "채팅방 제목", example = "스프링 스터디") String title, + @Schema(title = "최대 인원", example = "10") Integer maxCapacity, + @Schema(title = "채팅방 생성 시간", example = "2023-10-05 14:30:00") @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, + @Schema(title = "현재 인원", example = "5") int currentCapacity, + @Schema(title = "참여 여부", example = "true") Boolean isJoined ) { public static ChatRoomInfoRes of(Long chatRoomId, diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatroommember/res/ChatRoomMemberDetailRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatroommember/res/ChatRoomMemberDetailRes.java index 4b011a2..53be1df 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatroommember/res/ChatRoomMemberDetailRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatroommember/res/ChatRoomMemberDetailRes.java @@ -1,7 +1,12 @@ package com.rabbitmqprac.application.dto.chatroommember.res; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "ChatRoomMemberDetailRes", title = "채팅방 멤버 상세 DTO") public record ChatRoomMemberDetailRes( + @Schema(title = "유저 ID", example = "1") Long userId, + @Schema(title = "닉네임", example = "홍길동") String nickname ) { diff --git a/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignInReq.java b/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignInReq.java index c53dee6..5730a29 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignInReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignInReq.java @@ -1,9 +1,12 @@ package com.rabbitmqprac.application.dto.oauth.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +@Schema(name = "oauthSignInReq", title = "소셜 로그인 요청 DTO") public record OauthSignInReq( - @NotBlank(message = "OIDC CODE 필수 입력값입니다.") + @Schema(title = "OAUTH CODE", example = "4/P7q7W91a-oMsCeLvIaQm6bTrgtp7") + @NotBlank(message = "OAUTH CODE는 필수 입력값입니다.") String code ) { } diff --git a/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignUpReq.java b/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignUpReq.java index ce91d43..b6b9788 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignUpReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/oauth/req/OauthSignUpReq.java @@ -1,11 +1,15 @@ package com.rabbitmqprac.application.dto.oauth.req; import com.rabbitmqprac.global.annotation.Nickname; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +@Schema(name = "oauthSignUpReq", title = "소셜 회원가입 요청 DTO") public record OauthSignUpReq( - @NotBlank(message = "OIDC CODE는 필수 입력값입니다.") + @Schema(title = "OAUTH CODE", example = "4/P7q7W91a-oMsCeLvIaQm6bTrgtp7") + @NotBlank(message = "OAUTH CODE는 필수 입력값입니다.") String code, + @Schema(title = "닉네임", example = "RabbitMaster") @NotBlank(message = "닉네임을 입력해주세요") @Nickname String nickname diff --git a/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameCheckReq.java b/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameCheckReq.java index b24e13d..0aad597 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameCheckReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameCheckReq.java @@ -1,8 +1,11 @@ package com.rabbitmqprac.application.dto.user.req; import com.rabbitmqprac.global.annotation.Nickname; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(name = "nicknameCheckReq", title = "닉네임 중복 확인 요청 DTO") public record NicknameCheckReq( + @Schema(description = "닉네임", example = "RabbitMaster") @Nickname String nickname ) { diff --git a/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameUpdateReq.java b/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameUpdateReq.java index d032594..2254d93 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameUpdateReq.java +++ b/src/main/java/com/rabbitmqprac/application/dto/user/req/NicknameUpdateReq.java @@ -1,9 +1,12 @@ package com.rabbitmqprac.application.dto.user.req; import com.rabbitmqprac.global.annotation.Nickname; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +@Schema(name = "nicknameUpdateReq", title = "닉네임 변경 요청 DTO") public record NicknameUpdateReq( + @Schema(description = "닉네임", example = "RabbitMaster") @NotBlank(message = "닉네임을 입력해주세요") @Nickname String nickname diff --git a/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java b/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java new file mode 100644 index 0000000..6c460aa --- /dev/null +++ b/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java @@ -0,0 +1,66 @@ +package com.rabbitmqprac.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.filter.ForwardedHeaderFilter; + +import static org.springframework.security.config.Elements.JWT; + +@OpenAPIDefinition( + servers = { + @Server(url = "${rabbit.server.domain.local}", description = "Local Server"), + @Server(url = "${rabbit.server.domain.dev}", description = "Develop Server") + } +) +@Configuration +@RequiredArgsConstructor +public class SwaggerConfig { + private final Environment environment; + + @Bean + public OpenAPI openAPI() { + String[] profiles = environment.getActiveProfiles(); + String activeProfile = (profiles.length > 0) ? profiles[0] : "default"; + + SecurityRequirement securityRequirement = new SecurityRequirement().addList(JWT); + + return new OpenAPI() + .info(apiInfo(activeProfile)) + .addSecurityItem(securityRequirement) + .components(securitySchemes()); + } + + @Bean + public ForwardedHeaderFilter forwardedHeaderFilter() { + return new ForwardedHeaderFilter(); + } + + private Info apiInfo(String activeProfile) { + return new Info() + .title("Rabbit API (" + activeProfile + ")") + .description("채팅 플랫폼 Rabbit API 명세서") + .version("v1.0.0"); + } + + private Components securitySchemes() { + final var securitySchemeAccessToken = new SecurityScheme() + .name(JWT) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization"); + + return new Components() + .addSecuritySchemes(JWT, securitySchemeAccessToken); + } +} diff --git a/src/main/java/com/rabbitmqprac/global/exception/payload/ErrorResponse.java b/src/main/java/com/rabbitmqprac/global/exception/payload/ErrorResponse.java index 6e9cb2a..65727d2 100644 --- a/src/main/java/com/rabbitmqprac/global/exception/payload/ErrorResponse.java +++ b/src/main/java/com/rabbitmqprac/global/exception/payload/ErrorResponse.java @@ -1,6 +1,7 @@ package com.rabbitmqprac.global.exception.payload; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; import org.springframework.validation.BindingResult; @@ -9,11 +10,19 @@ import java.util.HashMap; import java.util.Map; +@Schema(title = "API 응답 - 실패 및 에러") @Builder @Getter public class ErrorResponse { + @Schema( + title = "에러 코드", + description = "정의된 에러의 4~6자리 정수형 문자열로 상태코드(3)+이유코드(1)+도메인코드(1)+필드코드(1)로 구성됩니다.", + example = "40401" + ) private String code; + @Schema(title = "에러 이유", description = "에러 코드의 이유코드에 해당하며, 에러 원인의 디테일한 상태값을 제공", example = "REQUESTED_RESOURCE_NOT_FOUND") private String reason; + @Schema(title = "에러 메시지", description = "에러 메시지", example = "회원을 찾을 수 없습니다.") private String message; @JsonInclude(JsonInclude.Include.NON_NULL) private Object fieldErrors; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9a26064..82f902e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,8 @@ +rabbit: + server: + domain: + local: ${RABBIT_SERVER_DOMAIN_LOCAL:localhost:8080} + dev: ${RABBIT_SERVER_DOMAIN_DEV:localhost:8080} spring: config: import: optional:file:.env[.properties] From 5a55736e7dc71c9811cda858c00500736705f883 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 19:20:12 +0900 Subject: [PATCH 02/11] =?UTF-8?q?chore:=20swagger=20error=20response=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=83=9D=EC=84=B1=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 82f902e..85eeff6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -86,6 +86,10 @@ rabbitmq: routing: key: "*.room." +# swagger에서 자동으로 error response를 생성하지 않도록 설정 +springdoc: + override-with-generic-response: false + logging: level: ROOT: INFO From 869efcb1d0f2ff33cd1a761d77c773dc32809141 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 20:14:10 +0900 Subject: [PATCH 03/11] =?UTF-8?q?chore:=20Swagger=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EB=B0=98=EC=9E=90=EB=8F=99=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 커스텀 어노테이션을 통한 swagger 문서화 자동 등록 --- .../rabbitmqprac/application/api/AuthApi.java | 34 ++++++ .../application/api/ChatMessageApi.java | 14 +++ .../application/api/ChatRoomApi.java | 7 ++ .../application/api/OauthApi.java | 25 +++++ .../rabbitmqprac/application/api/UserApi.java | 6 + .../rabbitmqprac/config/SwaggerConfig.java | 12 ++ .../annotation/ApiExceptionExplanation.java | 39 +++++++ .../annotation/ApiExceptionExplanations.java | 36 ++++++ .../util/ApiExceptionExplainParser.java | 103 ++++++++++++++++++ 9 files changed, 276 insertions(+) create mode 100644 src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanation.java create mode 100644 src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanations.java create mode 100644 src/main/java/com/rabbitmqprac/global/util/ApiExceptionExplainParser.java diff --git a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java index 482ec3a..a0f8011 100644 --- a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java @@ -3,6 +3,10 @@ import com.rabbitmqprac.application.dto.auth.req.AuthSignInReq; import com.rabbitmqprac.application.dto.auth.req.AuthSignUpReq; import com.rabbitmqprac.application.dto.auth.req.AuthUpdatePasswordReq; +import com.rabbitmqprac.domain.context.auth.exception.AuthErrorCode; +import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanations; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -17,11 +21,41 @@ public interface AuthApi { @Operation(summary = "일반 회원가입") + @ApiExceptionExplanations({ + @ApiExceptionExplanation( + errorCode = AuthErrorCode.class, + constants = "PASSWORD_CONFIRM_MISMATCH" + ), + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "CONFLICT_USERNAME" + ) + }) ResponseEntity> signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq); @Operation(summary = "일반 로그인") + @ApiExceptionExplanations({ + @ApiExceptionExplanation( + errorCode = AuthErrorCode.class, + constants = "INVALID_PASSWORD" + ), + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "NOT_FOUND" + ) + }) ResponseEntity> signIn(@RequestBody @Validated AuthSignInReq authSignInReq); @Operation(summary = "비밀번호 변경") + @ApiExceptionExplanations({ + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "NOT_FOUND" + ), + @ApiExceptionExplanation( + errorCode = AuthErrorCode.class, + constants = "INVALID_PASSWORD" + ) + }) ResponseEntity patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq); } diff --git a/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java b/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java index 59312aa..7bbbba0 100644 --- a/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/ChatMessageApi.java @@ -2,6 +2,10 @@ import com.rabbitmqprac.application.dto.chatmessage.req.ChatMessageReq; import com.rabbitmqprac.application.dto.chatmessage.res.ChatMessageDetailRes; +import com.rabbitmqprac.domain.context.chatroom.exception.ChatRoomErrorCode; +import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanations; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import com.rabbitmqprac.infra.security.principal.UserPrincipal; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +35,16 @@ public interface ChatMessageApi { - 인증: Header에 JWT 토큰 필요 """ ) + @ApiExceptionExplanations({ + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "NOT_FOUND" + ), + @ApiExceptionExplanation( + errorCode = ChatRoomErrorCode.class, + constants = "NOT_FOUND" + ) + }) void sendMessage(UserPrincipal principal, @DestinationVariable Long chatRoomId, @Validated ChatMessageReq message diff --git a/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java b/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java index c1439ca..1d86d57 100644 --- a/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java @@ -3,6 +3,9 @@ import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanations; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -20,6 +23,10 @@ public interface ChatRoomApi { @Operation(summary = "채팅방 생성") + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "NOT_FOUND" + ) ChatRoomDetailRes create( @AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated ChatRoomCreateReq chatRoomCreateReq diff --git a/src/main/java/com/rabbitmqprac/application/api/OauthApi.java b/src/main/java/com/rabbitmqprac/application/api/OauthApi.java index 81085a2..507f560 100644 --- a/src/main/java/com/rabbitmqprac/application/api/OauthApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/OauthApi.java @@ -2,7 +2,12 @@ import com.rabbitmqprac.application.dto.oauth.req.OauthSignInReq; import com.rabbitmqprac.application.dto.oauth.req.OauthSignUpReq; +import com.rabbitmqprac.domain.context.oauth.exception.OauthErrorCode; +import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; import com.rabbitmqprac.domain.persistence.oauth.constant.OauthProvider; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanations; +import com.rabbitmqprac.infra.security.exception.JwtErrorCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; @@ -21,6 +26,16 @@ public interface OauthApi { - [GOOGLE CODE 발급 URL](https://accounts.google.com/o/oauth2/v2/auth?client_id=248388975343-0oo0f79rrsqpf1k63ahpivkhd2rfu1jp.apps.googleusercontent.com&redirect_uri=http://localhost:8080&response_type=code&scope=openid%20email%20profile&nonce=example-nonce) """ ) + @ApiExceptionExplanations({ + @ApiExceptionExplanation( + errorCode = OauthErrorCode.class, + constants = {"MISSING_ISS", "INVALID_ISS", "MISSING_NONCE", "INVALID_ISS", "INVALID_AUD", "INVALID_NONCE"} + ), + @ApiExceptionExplanation( + errorCode = JwtErrorCode.class, + constants = "MALFORMED_TOKEN" + ) + }) ResponseEntity signIn( @RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignInReq req @@ -35,6 +50,16 @@ ResponseEntity signIn( - [GOOGLE CODE 발급 URL](https://accounts.google.com/o/oauth2/v2/auth?client_id=248388975343-0oo0f79rrsqpf1k63ahpivkhd2rfu1jp.apps.googleusercontent.com&redirect_uri=http://localhost:8080&response_type=code&scope=openid%20email%20profile&nonce=example-nonce) """ ) + @ApiExceptionExplanations({ + @ApiExceptionExplanation( + errorCode = OauthErrorCode.class, + constants = {"CONFLICT", "MISSING_ISS", "INVALID_ISS", "MISSING_NONCE", "INVALID_ISS", "INVALID_AUD", "INVALID_NONCE"} + ), + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "CONFLICT_USERNAME" + ) + }) ResponseEntity signUp( @RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignUpReq req diff --git a/src/main/java/com/rabbitmqprac/application/api/UserApi.java b/src/main/java/com/rabbitmqprac/application/api/UserApi.java index 9690533..b044363 100644 --- a/src/main/java/com/rabbitmqprac/application/api/UserApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/UserApi.java @@ -3,6 +3,8 @@ import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; +import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -28,6 +30,10 @@ public interface UserApi { Map isDuplicatedUsername(@RequestParam @Validated String username); @Operation(summary = "닉네임 변경") + @ApiExceptionExplanation( + errorCode = UserErrorCode.class, + constants = "CONFLICT_USERNAME" + ) ResponseEntity patchNickname( @AuthenticationPrincipal SecurityUserDetails user, @RequestBody NicknameUpdateReq nicknameUpdateReq diff --git a/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java b/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java index 6c460aa..849fe09 100644 --- a/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java +++ b/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java @@ -1,17 +1,21 @@ package com.rabbitmqprac.config; +import com.rabbitmqprac.global.util.ApiExceptionExplainParser; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import lombok.RequiredArgsConstructor; +import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.web.filter.ForwardedHeaderFilter; +import org.springframework.web.method.HandlerMethod; import static org.springframework.security.config.Elements.JWT; @@ -44,6 +48,14 @@ public ForwardedHeaderFilter forwardedHeaderFilter() { return new ForwardedHeaderFilter(); } + @Bean + public OperationCustomizer customizer() { + return (Operation operation, HandlerMethod handlerMethod) -> { + ApiExceptionExplainParser.parse(operation, handlerMethod); + return operation; + }; + } + private Info apiInfo(String activeProfile) { return new Info() .title("Rabbit API (" + activeProfile + ")") diff --git a/src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanation.java b/src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanation.java new file mode 100644 index 0000000..1dbedfc --- /dev/null +++ b/src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanation.java @@ -0,0 +1,39 @@ +package com.rabbitmqprac.global.annotation; + +import com.rabbitmqprac.global.exception.payload.BaseErrorCode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Swagger 문서화에 사용되는 API 예외 응답 데이터를 담기 위한 어노테이션입니다. + *
+ *
+ * - errorCodes: BaseErrorCode를 구현한 Enum 클래스 타입을 지정합니다.
+ * - constants: Enum 클래스의 상수명을 콤마(,)로 구분하여 여러 개 지정할 수 있습니다.
+ *   예시: "INVALID_INPUT,RESOURCE_NOT_FOUND"
+ * - mediaType: 응답 미디어 타입을 지정합니다. 기본값은 "application/json"입니다.
+ * 
+ * + *

+ * {@code
+ * @ApiExceptionExplanation(
+ *     errorCodes = OauthErrorCode.class,
+ *     constants = "MISSING_ISS, INVALID_ISS"
+ * )
+ * }
+ * 
+ * + * 여러 ErrorCode Enum을 지정해야 하는 경우 {@link ApiExceptionExplanations} 어노테이션을 사용합니다. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiExceptionExplanation { + Class errorCode(); + + String[] constants(); + + String mediaType() default "application/json"; +} diff --git a/src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanations.java b/src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanations.java new file mode 100644 index 0000000..a799a4a --- /dev/null +++ b/src/main/java/com/rabbitmqprac/global/annotation/ApiExceptionExplanations.java @@ -0,0 +1,36 @@ +package com.rabbitmqprac.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 여러 예외 응답 예시를 한 번에 문서화할 때 사용하는 어노테이션입니다. + *
+ *
+ * - value: {@link ApiExceptionExplanation} 어노테이션 배열을 받아 여러 예외 상황을 한 번에 기술할 수 있습니다.
+ * 
+ * + *

+ * {@code
+ * @ApiResponseExplanations({
+ *     @ApiExceptionExplanation(
+ *         errorCodes = OauthErrorCode.class,
+ *         constants = "MISSING_ISS, INVALID_ISS"
+ *     ),
+ *     @ApiExceptionExplanation(
+ *         errorCodes = OauthErrorCode.class,
+ *         constants = "EXPIRED_TOKEN"
+ *     )
+ * })
+ * }
+ * 
+ *

+ * 단일 예외만 문서화할 경우 {@link ApiExceptionExplanation}만 사용해도 됩니다. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiExceptionExplanations { + ApiExceptionExplanation[] value() default {}; +} diff --git a/src/main/java/com/rabbitmqprac/global/util/ApiExceptionExplainParser.java b/src/main/java/com/rabbitmqprac/global/util/ApiExceptionExplainParser.java new file mode 100644 index 0000000..32c0289 --- /dev/null +++ b/src/main/java/com/rabbitmqprac/global/util/ApiExceptionExplainParser.java @@ -0,0 +1,103 @@ +package com.rabbitmqprac.global.util; + +import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; +import com.rabbitmqprac.global.annotation.ApiExceptionExplanations; +import com.rabbitmqprac.global.exception.payload.BaseErrorCode; +import com.rabbitmqprac.global.exception.payload.CausedBy; +import com.rabbitmqprac.global.exception.payload.ErrorResponse; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import lombok.AccessLevel; +import lombok.Builder; +import org.springframework.util.StringUtils; +import org.springframework.web.method.HandlerMethod; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class ApiExceptionExplainParser { + public static void parse(Operation operation, HandlerMethod handlerMethod) { + ApiExceptionExplanations explanations = handlerMethod.getMethodAnnotation(ApiExceptionExplanations.class); + if (explanations != null) { + generateExceptionResponseDocs(operation, explanations.value()); + return; + } + ApiExceptionExplanation explanation = handlerMethod.getMethodAnnotation(ApiExceptionExplanation.class); + if (explanation != null) { + generateExceptionResponseDocs(operation, new ApiExceptionExplanation[]{explanation}); + } + } + + private static void generateExceptionResponseDocs(Operation operation, ApiExceptionExplanation[] exceptions) { + ApiResponses responses = operation.getResponses(); + + Map> holders = Arrays.stream(exceptions) + .flatMap(ApiExceptionExplainParser::expandExampleHolders) + .collect(Collectors.groupingBy(ExampleHolder::httpStatus)); + + addExamplesToResponses(responses, holders); + } + + private static Stream expandExampleHolders(ApiExceptionExplanation annotation) { + String[] constantsArr = annotation.constants(); + if (constantsArr == null || constantsArr.length == 0) { + return Stream.of(ExampleHolder.from(annotation, null)); + } + return Arrays.stream(constantsArr) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(constant -> ExampleHolder.from(annotation, constant)); + } + + private static void addExamplesToResponses(ApiResponses responses, Map> holders) { + holders.forEach((httpStatus, exampleHolders) -> { + Content content = new Content(); + MediaType mediaType = new MediaType(); + ApiResponse response = new ApiResponse(); + + exampleHolders.forEach(holder -> mediaType.addExamples(holder.name(), holder.holder())); + content.addMediaType("application/json", mediaType); + response.setContent(content); + + responses.addApiResponse(String.valueOf(httpStatus), response); + }); + } + + @Builder(access = AccessLevel.PRIVATE) + private record ExampleHolder(int httpStatus, String name, String mediaType, String description, Example holder) { + static ExampleHolder from(ApiExceptionExplanation annotation, String constantOverride) { + BaseErrorCode errorCode = getErrorCode(annotation, constantOverride); + + return ExampleHolder.builder() + .httpStatus(errorCode.causedBy().statusCode().getCode()) + .name(errorCode.getExplainError()) + .mediaType(annotation.mediaType()) + .holder(createExample(errorCode)) + .build(); + } + + @SuppressWarnings("unchecked") + public static & BaseErrorCode> E getErrorCode(ApiExceptionExplanation annotation, String constantOverride) { + Class enumClass = (Class) annotation.errorCode(); + String constant = constantOverride != null ? constantOverride : (annotation.constants().length > 0 ? annotation.constants()[0] : null); + return Enum.valueOf(enumClass, constant); + } + + private static Example createExample(BaseErrorCode errorCode) { + CausedBy causedBy = errorCode.causedBy(); + ErrorResponse response = ErrorResponse.of(causedBy.getCode(), causedBy.getReason(), errorCode.getExplainError()); + + Example example = new Example(); + example.setValue(response); + + return example; + } + } +} From 7b7cd452f40d0aa0824722028ca251603e135379 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 20:26:45 +0900 Subject: [PATCH 04/11] =?UTF-8?q?chore:=20ChatRoomInfoRes=20->=20ChatRoomS?= =?UTF-8?q?ummaryRes=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/api/ChatRoomApi.java | 9 ++------- .../controller/ChatRoomController.java | 4 ++-- ...tRoomInfoRes.java => ChatRoomSummaryRes.java} | 16 ++++++++-------- .../application/mapper/ChatRoomMapper.java | 6 +++--- .../chatroom/service/ChatRoomService.java | 4 ++-- .../service/ChatRoomServiceTest.java | 10 +++++----- 6 files changed, 22 insertions(+), 27 deletions(-) rename src/main/java/com/rabbitmqprac/application/dto/chatroom/res/{ChatRoomInfoRes.java => ChatRoomSummaryRes.java} (68%) diff --git a/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java b/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java index 1d86d57..fad6c9b 100644 --- a/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/ChatRoomApi.java @@ -2,22 +2,17 @@ import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; -import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomSummaryRes; import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; -import com.rabbitmqprac.global.annotation.ApiExceptionExplanations; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import java.util.List; -import java.util.Objects; -import java.util.Optional; @Tag(name = "[채팅방 API]") public interface ChatRoomApi { @@ -36,5 +31,5 @@ ChatRoomDetailRes create( List getMyChatRooms(@AuthenticationPrincipal SecurityUserDetails user); @Operation(summary = "채팅방 목록 조회") - List getChatRooms(@AuthenticationPrincipal SecurityUserDetails user); + List getChatRooms(@AuthenticationPrincipal SecurityUserDetails user); } diff --git a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java index 7f60054..312066a 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomController.java @@ -3,7 +3,7 @@ import com.rabbitmqprac.application.api.ChatRoomApi; import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; -import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomSummaryRes; import com.rabbitmqprac.domain.context.chatroom.service.ChatRoomService; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import lombok.RequiredArgsConstructor; @@ -41,7 +41,7 @@ public List getMyChatRooms(@AuthenticationPrincipal SecurityU @Override @GetMapping("/chat-rooms") - public List getChatRooms(@AuthenticationPrincipal SecurityUserDetails user) { + public List getChatRooms(@AuthenticationPrincipal SecurityUserDetails user) { return chatRoomService.getChatRooms(Optional.ofNullable(Objects.isNull(user) ? null : user.getUserId()) ); } diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java b/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomSummaryRes.java similarity index 68% rename from src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java rename to src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomSummaryRes.java index f36c37c..fe00b41 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomInfoRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/chatroom/res/ChatRoomSummaryRes.java @@ -8,7 +8,7 @@ import java.time.LocalDateTime; @Schema(name = "ChatRoomInfoRes", title = "채팅방 정보 DTO") -public record ChatRoomInfoRes( +public record ChatRoomSummaryRes( @Schema(title = "채팅방 ID", example = "1") Long chatRoomId, @Schema(title = "채팅방 제목", example = "스프링 스터디") @@ -26,13 +26,13 @@ public record ChatRoomInfoRes( @Schema(title = "참여 여부", example = "true") Boolean isJoined ) { - public static ChatRoomInfoRes of(Long chatRoomId, - String title, - Integer maxCapacity, - LocalDateTime createdAt, - int currentCapacity, - Boolean isJoined + public static ChatRoomSummaryRes of(Long chatRoomId, + String title, + Integer maxCapacity, + LocalDateTime createdAt, + int currentCapacity, + Boolean isJoined ) { - return new ChatRoomInfoRes(chatRoomId, title, maxCapacity, createdAt, currentCapacity, isJoined); + return new ChatRoomSummaryRes(chatRoomId, title, maxCapacity, createdAt, currentCapacity, isJoined); } } diff --git a/src/main/java/com/rabbitmqprac/application/mapper/ChatRoomMapper.java b/src/main/java/com/rabbitmqprac/application/mapper/ChatRoomMapper.java index de1acfb..ffb1514 100644 --- a/src/main/java/com/rabbitmqprac/application/mapper/ChatRoomMapper.java +++ b/src/main/java/com/rabbitmqprac/application/mapper/ChatRoomMapper.java @@ -2,7 +2,7 @@ import com.rabbitmqprac.application.dto.chatmessage.res.LastChatMessageDetailRes; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; -import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomSummaryRes; import com.rabbitmqprac.domain.persistence.chatroom.entity.ChatRoom; import com.rabbitmqprac.global.annotation.Mapper; @@ -39,8 +39,8 @@ public static ChatRoomDetailRes toDetailRes(ChatRoom chatRoom, ); } - public static ChatRoomInfoRes toInfoRes(ChatRoom chatRoom, int currentCapacity, Boolean isJoined) { - return ChatRoomInfoRes.of( + public static ChatRoomSummaryRes toInfoRes(ChatRoom chatRoom, int currentCapacity, Boolean isJoined) { + return ChatRoomSummaryRes.of( chatRoom.getId(), chatRoom.getTitle(), chatRoom.getMaxCapacity(), diff --git a/src/main/java/com/rabbitmqprac/domain/context/chatroom/service/ChatRoomService.java b/src/main/java/com/rabbitmqprac/domain/context/chatroom/service/ChatRoomService.java index af0573d..1c9d20b 100644 --- a/src/main/java/com/rabbitmqprac/domain/context/chatroom/service/ChatRoomService.java +++ b/src/main/java/com/rabbitmqprac/domain/context/chatroom/service/ChatRoomService.java @@ -3,7 +3,7 @@ import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; -import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomSummaryRes; import com.rabbitmqprac.application.mapper.ChatMessageMapper; import com.rabbitmqprac.application.mapper.ChatRoomMapper; import com.rabbitmqprac.domain.context.chatmessage.service.ChatMessageService; @@ -73,7 +73,7 @@ public List getMyChatRooms(Long userId) { } @Transactional(readOnly = true) - public List getChatRooms(Optional userId) { + public List getChatRooms(Optional userId) { List chatRooms = chatRoomRepository.findAll(); return chatRooms.stream() diff --git a/src/test/java/com/rabbitmqprac/service/ChatRoomServiceTest.java b/src/test/java/com/rabbitmqprac/service/ChatRoomServiceTest.java index 0c66c73..86956e9 100644 --- a/src/test/java/com/rabbitmqprac/service/ChatRoomServiceTest.java +++ b/src/test/java/com/rabbitmqprac/service/ChatRoomServiceTest.java @@ -2,7 +2,7 @@ import com.rabbitmqprac.application.dto.chatroom.req.ChatRoomCreateReq; import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomDetailRes; -import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomInfoRes; +import com.rabbitmqprac.application.dto.chatroom.res.ChatRoomSummaryRes; import com.rabbitmqprac.common.fixture.ChatRoomFixture; import com.rabbitmqprac.common.fixture.UserFixture; import com.rabbitmqprac.domain.context.chatmessage.service.ChatMessageService; @@ -142,13 +142,13 @@ void getChatRoomsWhenNotLoggedIn() { given(chatRoomRepository.findAll()).willReturn(List.of(chatRoom)); // when - List result = chatRoomService.getChatRooms(Optional.ofNullable(null)); + List result = chatRoomService.getChatRooms(Optional.ofNullable(null)); // then assertThat(result).isNotNull(); assertThat(result.size()).isEqualTo(1); - ChatRoomInfoRes res = result.getFirst(); + ChatRoomSummaryRes res = result.getFirst(); assertThat(res.isJoined()).isFalse(); } @@ -160,13 +160,13 @@ void getChatRoomsWhenLoggedIn() { given(chatRoomMemberService.isExists(chatRoom.getId(), user.getId())).willReturn(true); // when - List result = chatRoomService.getChatRooms(Optional.of(user.getId())); + List result = chatRoomService.getChatRooms(Optional.of(user.getId())); // then assertThat(result).isNotNull(); assertThat(result.size()).isEqualTo(1); - ChatRoomInfoRes res = result.getFirst(); + ChatRoomSummaryRes res = result.getFirst(); assertThat(res.isJoined()).isTrue(); } } From b2d86f79f43680ffd7c24f83e8803dfeeb57c436 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 20:42:42 +0900 Subject: [PATCH 05/11] =?UTF-8?q?chore:=20=EC=9D=91=EB=8B=B5=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=97=86=EB=8A=94=20API=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=B4=20NO=5FCONTENT=20Status=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/rabbitmqprac/application/api/AuthApi.java | 2 +- .../java/com/rabbitmqprac/application/api/UserApi.java | 2 +- .../application/controller/AuthController.java | 6 ++++-- .../application/controller/ChatRoomMemberController.java | 3 +++ .../application/controller/UserController.java | 9 +++++---- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java index a0f8011..6b4317d 100644 --- a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java @@ -57,5 +57,5 @@ public interface AuthApi { constants = "INVALID_PASSWORD" ) }) - ResponseEntity patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq); + void patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq); } diff --git a/src/main/java/com/rabbitmqprac/application/api/UserApi.java b/src/main/java/com/rabbitmqprac/application/api/UserApi.java index b044363..7b42c70 100644 --- a/src/main/java/com/rabbitmqprac/application/api/UserApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/UserApi.java @@ -34,7 +34,7 @@ public interface UserApi { errorCode = UserErrorCode.class, constants = "CONFLICT_USERNAME" ) - ResponseEntity patchNickname( + void patchNickname( @AuthenticationPrincipal SecurityUserDetails user, @RequestBody NicknameUpdateReq nicknameUpdateReq diff --git a/src/main/java/com/rabbitmqprac/application/controller/AuthController.java b/src/main/java/com/rabbitmqprac/application/controller/AuthController.java index 262aa03..61d16e1 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/AuthController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/AuthController.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.time.Duration; @@ -44,11 +46,11 @@ public ResponseEntity> signIn(@RequestBody @Validated AuthSign return createAuthenticatedResponse(authService.signIn(authSignInReq)); } + @ResponseStatus(HttpStatus.NO_CONTENT) @Override @PatchMapping("/password") - public ResponseEntity patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq) { + public void patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @RequestBody @Validated AuthUpdatePasswordReq authUpdatePasswordReq) { authService.updatePassword(user.getUserId(), authUpdatePasswordReq); - return ResponseEntity.noContent().build(); } private ResponseEntity> createAuthenticatedResponse(Pair userInfo) { diff --git a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java index 3642373..fe4e371 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/ChatRoomMemberController.java @@ -5,10 +5,12 @@ import com.rabbitmqprac.domain.context.chatroommember.service.ChatRoomMemberService; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -19,6 +21,7 @@ public class ChatRoomMemberController implements ChatRoomMemberApi { private final ChatRoomMemberService chatRoomMemberService; + @ResponseStatus(HttpStatus.NO_CONTENT) @PostMapping("/chat-rooms/{chatRoomId}/members") public void joinChatRoom(@AuthenticationPrincipal SecurityUserDetails user, @PathVariable Long chatRoomId) { diff --git a/src/main/java/com/rabbitmqprac/application/controller/UserController.java b/src/main/java/com/rabbitmqprac/application/controller/UserController.java index a6d7f30..4f029f1 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/UserController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/UserController.java @@ -8,13 +8,14 @@ import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -44,12 +45,12 @@ public Map isDuplicatedUsername(@RequestParam @Validated String return Map.of("isDuplicated", userService.isDuplicatedUsername(username)); } + @ResponseStatus(HttpStatus.NO_CONTENT) @Override @PatchMapping("/users/nickname") - public ResponseEntity patchNickname(@AuthenticationPrincipal SecurityUserDetails user, - @RequestBody NicknameUpdateReq nicknameUpdateReq) { + public void patchNickname(@AuthenticationPrincipal SecurityUserDetails user, + @RequestBody NicknameUpdateReq nicknameUpdateReq) { userService.updateNickname(user.getUserId(), nicknameUpdateReq); - return ResponseEntity.noContent().build(); } @Override From 5631611d6b30a4947feb316dab1ecdb401fcd289 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 20:48:30 +0900 Subject: [PATCH 06/11] =?UTF-8?q?chore:=20UserDetailRes=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/{auth => user}/res/UserDetailRes.java | 2 +- .../java/com/rabbitmqprac/application/mapper/UserMapper.java | 2 +- .../rabbitmqprac/domain/context/user/service/UserService.java | 2 +- src/test/java/com/rabbitmqprac/service/UserServiceTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/com/rabbitmqprac/application/dto/{auth => user}/res/UserDetailRes.java (89%) diff --git a/src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java b/src/main/java/com/rabbitmqprac/application/dto/user/res/UserDetailRes.java similarity index 89% rename from src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java rename to src/main/java/com/rabbitmqprac/application/dto/user/res/UserDetailRes.java index be2112f..c561d37 100644 --- a/src/main/java/com/rabbitmqprac/application/dto/auth/res/UserDetailRes.java +++ b/src/main/java/com/rabbitmqprac/application/dto/user/res/UserDetailRes.java @@ -1,4 +1,4 @@ -package com.rabbitmqprac.application.dto.auth.res; +package com.rabbitmqprac.application.dto.user.res; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/rabbitmqprac/application/mapper/UserMapper.java b/src/main/java/com/rabbitmqprac/application/mapper/UserMapper.java index 6b75b17..bc0ce63 100644 --- a/src/main/java/com/rabbitmqprac/application/mapper/UserMapper.java +++ b/src/main/java/com/rabbitmqprac/application/mapper/UserMapper.java @@ -1,6 +1,6 @@ package com.rabbitmqprac.application.mapper; -import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; +import com.rabbitmqprac.application.dto.user.res.UserDetailRes; import com.rabbitmqprac.domain.persistence.user.entity.User; import com.rabbitmqprac.global.annotation.Mapper; diff --git a/src/main/java/com/rabbitmqprac/domain/context/user/service/UserService.java b/src/main/java/com/rabbitmqprac/domain/context/user/service/UserService.java index c591fcc..a79e022 100644 --- a/src/main/java/com/rabbitmqprac/domain/context/user/service/UserService.java +++ b/src/main/java/com/rabbitmqprac/domain/context/user/service/UserService.java @@ -1,7 +1,7 @@ package com.rabbitmqprac.domain.context.user.service; -import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; +import com.rabbitmqprac.application.dto.user.res.UserDetailRes; import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; import com.rabbitmqprac.application.mapper.UserMapper; diff --git a/src/test/java/com/rabbitmqprac/service/UserServiceTest.java b/src/test/java/com/rabbitmqprac/service/UserServiceTest.java index cc3720c..f928f74 100644 --- a/src/test/java/com/rabbitmqprac/service/UserServiceTest.java +++ b/src/test/java/com/rabbitmqprac/service/UserServiceTest.java @@ -1,6 +1,6 @@ package com.rabbitmqprac.service; -import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; +import com.rabbitmqprac.application.dto.user.res.UserDetailRes; import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; import com.rabbitmqprac.common.fixture.UserFixture; From a8bb59653d3c9a144bb64cdce1fac5f8ac7c84cb Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 20:49:22 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore:=20ChatRoomMemberCreateReq=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/chatroommember/req/ChatRoomMemberCreateReq.java | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/main/java/com/rabbitmqprac/application/dto/chatroommember/req/ChatRoomMemberCreateReq.java diff --git a/src/main/java/com/rabbitmqprac/application/dto/chatroommember/req/ChatRoomMemberCreateReq.java b/src/main/java/com/rabbitmqprac/application/dto/chatroommember/req/ChatRoomMemberCreateReq.java deleted file mode 100644 index 77f0051..0000000 --- a/src/main/java/com/rabbitmqprac/application/dto/chatroommember/req/ChatRoomMemberCreateReq.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.rabbitmqprac.application.dto.chatroommember.req; - -import lombok.Getter; - -@Getter -public class ChatRoomMemberCreateReq { - private Long userId; -} From ad25ff9e2fa097f4947e708a846cc6e226705564 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 21:04:52 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore:=20rabbit=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B0=92=EC=97=90=20http=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=86=A0=EC=BD=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 85eeff6..83e2b6f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,8 +1,8 @@ rabbit: server: domain: - local: ${RABBIT_SERVER_DOMAIN_LOCAL:localhost:8080} - dev: ${RABBIT_SERVER_DOMAIN_DEV:localhost:8080} + local: ${RABBIT_SERVER_DOMAIN_LOCAL:http://localhost:8080} + dev: ${RABBIT_SERVER_DOMAIN_LOCAL:http://localhost:8080} spring: config: import: optional:file:.env[.properties] From 5df2ffa24be684d9fb7c6801b26195a57ea39655 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 21:08:19 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=9D=91=EB=8B=B5=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20AuthenticationResponseUtil?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rabbitmqprac/application/api/AuthApi.java | 5 +-- .../application/api/OauthApi.java | 5 +-- .../rabbitmqprac/application/api/UserApi.java | 3 +- .../controller/AuthController.java | 30 ++++-------------- .../controller/OauthController.java | 31 ++++--------------- .../controller/UserController.java | 2 +- .../application/dto/user/res/UserIdRes.java | 13 ++++++++ .../util/AuthenticationResponseUtil.java | 27 ++++++++++++++++ 8 files changed, 60 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/rabbitmqprac/application/dto/user/res/UserIdRes.java create mode 100644 src/main/java/com/rabbitmqprac/global/util/AuthenticationResponseUtil.java diff --git a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java index 6b4317d..aeb425b 100644 --- a/src/main/java/com/rabbitmqprac/application/api/AuthApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/AuthApi.java @@ -3,6 +3,7 @@ import com.rabbitmqprac.application.dto.auth.req.AuthSignInReq; import com.rabbitmqprac.application.dto.auth.req.AuthSignUpReq; import com.rabbitmqprac.application.dto.auth.req.AuthUpdatePasswordReq; +import com.rabbitmqprac.application.dto.user.res.UserIdRes; import com.rabbitmqprac.domain.context.auth.exception.AuthErrorCode; import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; import com.rabbitmqprac.global.annotation.ApiExceptionExplanation; @@ -31,7 +32,7 @@ public interface AuthApi { constants = "CONFLICT_USERNAME" ) }) - ResponseEntity> signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq); + ResponseEntity signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq); @Operation(summary = "일반 로그인") @ApiExceptionExplanations({ @@ -44,7 +45,7 @@ public interface AuthApi { constants = "NOT_FOUND" ) }) - ResponseEntity> signIn(@RequestBody @Validated AuthSignInReq authSignInReq); + ResponseEntity signIn(@RequestBody @Validated AuthSignInReq authSignInReq); @Operation(summary = "비밀번호 변경") @ApiExceptionExplanations({ diff --git a/src/main/java/com/rabbitmqprac/application/api/OauthApi.java b/src/main/java/com/rabbitmqprac/application/api/OauthApi.java index 507f560..3958e75 100644 --- a/src/main/java/com/rabbitmqprac/application/api/OauthApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/OauthApi.java @@ -2,6 +2,7 @@ import com.rabbitmqprac.application.dto.oauth.req.OauthSignInReq; import com.rabbitmqprac.application.dto.oauth.req.OauthSignUpReq; +import com.rabbitmqprac.application.dto.user.res.UserIdRes; import com.rabbitmqprac.domain.context.oauth.exception.OauthErrorCode; import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; import com.rabbitmqprac.domain.persistence.oauth.constant.OauthProvider; @@ -36,7 +37,7 @@ public interface OauthApi { constants = "MALFORMED_TOKEN" ) }) - ResponseEntity signIn( + ResponseEntity signIn( @RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignInReq req ); @@ -60,7 +61,7 @@ ResponseEntity signIn( constants = "CONFLICT_USERNAME" ) }) - ResponseEntity signUp( + ResponseEntity signUp( @RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignUpReq req ); diff --git a/src/main/java/com/rabbitmqprac/application/api/UserApi.java b/src/main/java/com/rabbitmqprac/application/api/UserApi.java index 7b42c70..776025b 100644 --- a/src/main/java/com/rabbitmqprac/application/api/UserApi.java +++ b/src/main/java/com/rabbitmqprac/application/api/UserApi.java @@ -1,6 +1,6 @@ package com.rabbitmqprac.application.api; -import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; +import com.rabbitmqprac.application.dto.user.res.UserDetailRes; import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; import com.rabbitmqprac.domain.context.user.exception.UserErrorCode; @@ -8,7 +8,6 @@ import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; diff --git a/src/main/java/com/rabbitmqprac/application/controller/AuthController.java b/src/main/java/com/rabbitmqprac/application/controller/AuthController.java index 61d16e1..94b5eba 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/AuthController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/AuthController.java @@ -4,16 +4,13 @@ import com.rabbitmqprac.application.dto.auth.req.AuthSignInReq; import com.rabbitmqprac.application.dto.auth.req.AuthSignUpReq; import com.rabbitmqprac.application.dto.auth.req.AuthUpdatePasswordReq; +import com.rabbitmqprac.application.dto.user.res.UserIdRes; import com.rabbitmqprac.domain.context.auth.service.AuthService; -import com.rabbitmqprac.global.util.CookieUtil; +import com.rabbitmqprac.global.util.AuthenticationResponseUtil; import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails; -import com.rabbitmqprac.infra.security.jwt.Jwts; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; @@ -24,9 +21,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import java.time.Duration; -import java.util.Map; - @Slf4j @RequiredArgsConstructor @RequestMapping("/auth") @@ -36,14 +30,14 @@ public class AuthController implements AuthApi { @Override @PostMapping("/sign-up") - public ResponseEntity> signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq) { - return createAuthenticatedResponse(authService.signUp(authSignUpReq)); + public ResponseEntity signUp(@RequestBody @Validated AuthSignUpReq authSignUpReq) { + return AuthenticationResponseUtil.createAuthenticatedResponse(authService.signUp(authSignUpReq)); } @Override @PostMapping("/sign-in") - public ResponseEntity> signIn(@RequestBody @Validated AuthSignInReq authSignInReq) { - return createAuthenticatedResponse(authService.signIn(authSignInReq)); + public ResponseEntity signIn(@RequestBody @Validated AuthSignInReq authSignInReq) { + return AuthenticationResponseUtil.createAuthenticatedResponse(authService.signIn(authSignInReq)); } @ResponseStatus(HttpStatus.NO_CONTENT) @@ -53,17 +47,5 @@ public void patchPassword(@AuthenticationPrincipal SecurityUserDetails user, @Re authService.updatePassword(user.getUserId(), authUpdatePasswordReq); } - private ResponseEntity> createAuthenticatedResponse(Pair userInfo) { - ResponseCookie cookie = CookieUtil.createCookie( - "refreshToken", userInfo.getValue().refreshToken(), Duration.ofDays(7).toSeconds() - ); - - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) - .header(HttpHeaders.AUTHORIZATION, userInfo.getValue().accessToken()) - .body( - Map.of("userId", userInfo.getKey()) - ); - } } diff --git a/src/main/java/com/rabbitmqprac/application/controller/OauthController.java b/src/main/java/com/rabbitmqprac/application/controller/OauthController.java index 2f57264..17ea1f7 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/OauthController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/OauthController.java @@ -3,14 +3,11 @@ import com.rabbitmqprac.application.api.OauthApi; import com.rabbitmqprac.application.dto.oauth.req.OauthSignInReq; import com.rabbitmqprac.application.dto.oauth.req.OauthSignUpReq; +import com.rabbitmqprac.application.dto.user.res.UserIdRes; import com.rabbitmqprac.domain.context.oauth.service.OauthService; import com.rabbitmqprac.domain.persistence.oauth.constant.OauthProvider; -import com.rabbitmqprac.global.util.CookieUtil; -import com.rabbitmqprac.infra.security.jwt.Jwts; +import com.rabbitmqprac.global.util.AuthenticationResponseUtil; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; @@ -19,9 +16,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.time.Duration; -import java.util.Map; - @RequiredArgsConstructor @RestController public class OauthController implements OauthApi { @@ -30,27 +24,14 @@ public class OauthController implements OauthApi { @Override @PreAuthorize("isAnonymous()") @PostMapping("/oauth/sign-in") - public ResponseEntity signIn(@RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignInReq req) { - return createAuthenticatedResponse(oauthService.signIn(oauthProvider, req)); + public ResponseEntity signIn(@RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignInReq req) { + return AuthenticationResponseUtil.createAuthenticatedResponse(oauthService.signIn(oauthProvider, req)); } @Override @PreAuthorize("isAnonymous()") @PostMapping("/oauth/sign-up") - public ResponseEntity signUp(@RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignUpReq req) { - return createAuthenticatedResponse(oauthService.signUp(oauthProvider, req)); - } - - private ResponseEntity createAuthenticatedResponse(Pair userInfo) { - ResponseCookie cookie = CookieUtil.createCookie( - "refreshToken", userInfo.getValue().refreshToken(), Duration.ofDays(7).toSeconds() - ); - - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) - .header(HttpHeaders.AUTHORIZATION, userInfo.getValue().accessToken()) - .body( - Map.of("userId", userInfo.getKey()) - ); + public ResponseEntity signUp(@RequestParam OauthProvider oauthProvider, @RequestBody @Validated OauthSignUpReq req) { + return AuthenticationResponseUtil.createAuthenticatedResponse(oauthService.signUp(oauthProvider, req)); } } diff --git a/src/main/java/com/rabbitmqprac/application/controller/UserController.java b/src/main/java/com/rabbitmqprac/application/controller/UserController.java index 4f029f1..24e84c5 100644 --- a/src/main/java/com/rabbitmqprac/application/controller/UserController.java +++ b/src/main/java/com/rabbitmqprac/application/controller/UserController.java @@ -1,7 +1,7 @@ package com.rabbitmqprac.application.controller; import com.rabbitmqprac.application.api.UserApi; -import com.rabbitmqprac.application.dto.auth.res.UserDetailRes; +import com.rabbitmqprac.application.dto.user.res.UserDetailRes; import com.rabbitmqprac.application.dto.user.req.NicknameCheckReq; import com.rabbitmqprac.application.dto.user.req.NicknameUpdateReq; import com.rabbitmqprac.domain.context.user.service.UserService; diff --git a/src/main/java/com/rabbitmqprac/application/dto/user/res/UserIdRes.java b/src/main/java/com/rabbitmqprac/application/dto/user/res/UserIdRes.java new file mode 100644 index 0000000..069d202 --- /dev/null +++ b/src/main/java/com/rabbitmqprac/application/dto/user/res/UserIdRes.java @@ -0,0 +1,13 @@ +package com.rabbitmqprac.application.dto.user.res; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "userIdRes", title = "회원 ID 응답 DTO") +public record UserIdRes( + @Schema(title = "유저 ID", example = "1") + Long userId +) { + public static UserIdRes of(Long userId) { + return new UserIdRes(userId); + } +} diff --git a/src/main/java/com/rabbitmqprac/global/util/AuthenticationResponseUtil.java b/src/main/java/com/rabbitmqprac/global/util/AuthenticationResponseUtil.java new file mode 100644 index 0000000..c028e8e --- /dev/null +++ b/src/main/java/com/rabbitmqprac/global/util/AuthenticationResponseUtil.java @@ -0,0 +1,27 @@ +package com.rabbitmqprac.global.util; + +import com.rabbitmqprac.application.dto.user.res.UserIdRes; +import com.rabbitmqprac.global.annotation.Util; +import com.rabbitmqprac.infra.security.jwt.Jwts; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; + +import java.time.Duration; + +@Util +public final class AuthenticationResponseUtil { + public static ResponseEntity createAuthenticatedResponse(Pair userInfo) { + ResponseCookie cookie = CookieUtil.createCookie( + "refreshToken", userInfo.getValue().refreshToken(), Duration.ofDays(7).toSeconds() + ); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .header(HttpHeaders.AUTHORIZATION, userInfo.getValue().accessToken()) + .body( + UserIdRes.of(userInfo.getKey()) + ); + } +} From fcb1f7bcca7f7702221d211bc2d6b8668ed9492f Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 21:49:18 +0900 Subject: [PATCH 10/11] =?UTF-8?q?chore:=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EC=97=90=20GitHub=20=EC=99=B8=EB=B6=80=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/rabbitmqprac/config/SwaggerConfig.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java b/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java index 849fe09..7371999 100644 --- a/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java +++ b/src/main/java/com/rabbitmqprac/config/SwaggerConfig.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.info.Info; @@ -40,7 +41,11 @@ public OpenAPI openAPI() { return new OpenAPI() .info(apiInfo(activeProfile)) .addSecurityItem(securityRequirement) - .components(securitySchemes()); + .components(securitySchemes()) + .externalDocs(new ExternalDocumentation() + .description("Rabbit API GitHub") + .url("https://github.com/lsh2613/RabbitChat") + ); } @Bean From 9c3d4cb4f7fb7b5c7e0e05f743c6ba258b992311 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Tue, 26 Aug 2025 22:03:03 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore:=20AbbreviationAsWordInName=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checkstyle/config/rules.xml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/checkstyle/config/rules.xml b/checkstyle/config/rules.xml index 3e9622a..ac826bb 100644 --- a/checkstyle/config/rules.xml +++ b/checkstyle/config/rules.xml @@ -53,15 +53,6 @@ The following rules in the Naver coding convention cannot be checked by this con - - - - - - - -