From 68f60f6b85dd1e69a669b4e2898e56fb6cf846d2 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 04:19:37 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[chore]=20Spring=20docs=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index a908a53bd..f85de9560 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,9 @@ dependencies { //Google Guava implementation 'com.google.guava:guava:33.2.0-jre' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' } def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile From 2146259a9cb71cb5852ef2b6a661fdebc9bb6636 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 04:48:37 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[chore]=20Swagger=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/swagger/ExampleHolder.java | 14 ++ .../swagger/SwaggerResponseDescription.java | 61 ++++++++ .../annotation/ExceptionDescription.java | 15 ++ .../konkuk/thip/config/OpenApiConfig.java | 139 ++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/swagger/ExampleHolder.java create mode 100644 src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java create mode 100644 src/main/java/konkuk/thip/common/swagger/annotation/ExceptionDescription.java create mode 100644 src/main/java/konkuk/thip/config/OpenApiConfig.java diff --git a/src/main/java/konkuk/thip/common/swagger/ExampleHolder.java b/src/main/java/konkuk/thip/common/swagger/ExampleHolder.java new file mode 100644 index 000000000..5ed307bee --- /dev/null +++ b/src/main/java/konkuk/thip/common/swagger/ExampleHolder.java @@ -0,0 +1,14 @@ +package konkuk.thip.common.swagger; + +import io.swagger.v3.oas.models.examples.Example; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ExampleHolder { + + private Example holder; + private String name; + private int code; +} diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java new file mode 100644 index 000000000..baa9b583f --- /dev/null +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -0,0 +1,61 @@ +package konkuk.thip.common.swagger; + +import konkuk.thip.common.exception.code.ErrorCode; +import lombok.Getter; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static konkuk.thip.common.exception.code.ErrorCode.*; + +@Getter +public enum SwaggerResponseDescription { +// +// //Auth + LOGIN(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND + ))), + LOGOUT(new LinkedHashSet<>(Set.of( + + ))), + +// //User + USER_SIGNUP(new LinkedHashSet<>(Set.of( + ))), + USER_CHOICE_ALIAS(new LinkedHashSet<>(Set.of( + ))), + + // Follow + GET_USER_FOLLOW(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND + ))), + + + ; + private final Set errorCodeList; + SwaggerResponseDescription(Set errorCodeList) { + // 공통 에러 + errorCodeList.addAll(new LinkedHashSet<>(Set.of( + API_NOT_FOUND, + API_METHOD_NOT_ALLOWED, + API_SERVER_ERROR, + +// API_BAD_REQUEST, +// API_MISSING_PARAM, +// API_INVALID_PARAM, +// API_INVALID_TYPE + + AUTH_INVALID_TOKEN, + AUTH_EXPIRED_TOKEN, + AUTH_UNAUTHORIZED, + AUTH_TOKEN_NOT_FOUND +// AUTH_LOGIN_FAILED, +// AUTH_UNSUPPORTED_SOCIAL_LOGIN, + +// JSON_PROCESSING_ERROR + ))); + + + this.errorCodeList = errorCodeList; + } +} diff --git a/src/main/java/konkuk/thip/common/swagger/annotation/ExceptionDescription.java b/src/main/java/konkuk/thip/common/swagger/annotation/ExceptionDescription.java new file mode 100644 index 000000000..f12c1a981 --- /dev/null +++ b/src/main/java/konkuk/thip/common/swagger/annotation/ExceptionDescription.java @@ -0,0 +1,15 @@ +package konkuk.thip.common.swagger.annotation; + +import konkuk.thip.common.swagger.SwaggerResponseDescription; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExceptionDescription { + + SwaggerResponseDescription value(); +} diff --git a/src/main/java/konkuk/thip/config/OpenApiConfig.java b/src/main/java/konkuk/thip/config/OpenApiConfig.java new file mode 100644 index 000000000..23cabe1eb --- /dev/null +++ b/src/main/java/konkuk/thip/config/OpenApiConfig.java @@ -0,0 +1,139 @@ +package konkuk.thip.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +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.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 io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import konkuk.thip.common.dto.ErrorResponse; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.swagger.ExampleHolder; +import konkuk.thip.common.swagger.SwaggerResponseDescription; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.web.method.HandlerMethod; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.stream.Collectors.groupingBy; + +@OpenAPIDefinition( + info = @Info( + title = "Thip 백엔드 API 명세서", + description = "Springdoc을 이용한 Thip Swagger API 문서입니다.", + version = "1.0.0" + ) +) +@Configuration +public class OpenApiConfig { + private final String securitySchemaName = "JWT"; + + @Value("${server.https-url}") private String httpsUrl; + + @Value("${server.http-url}") private String httpUrl; + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .servers(List.of( + new Server().url(httpsUrl).description("HTTPS 배포 서버"), + new Server().url(httpUrl).description("HTTP IP"), + new Server().url("http://localhost:8080").description("로컬 개발 서버") + )) + .components(setComponents()) + .addSecurityItem(setSecurityItems()); + } + private Components setComponents() { + return new Components() + .addSecuritySchemes(securitySchemaName, bearerAuth()); + } + + private SecurityScheme bearerAuth() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat(securitySchemaName) + .in(SecurityScheme.In.HEADER) + .name(HttpHeaders.AUTHORIZATION); + } + + private SecurityRequirement setSecurityItems() { + return new SecurityRequirement() + .addList(securitySchemaName); + } + + @Bean + public OperationCustomizer customize() { + return (Operation operation, HandlerMethod handlerMethod) -> { + + ExceptionDescription exceptionDescription = handlerMethod.getMethodAnnotation( + ExceptionDescription.class); + + // ExceptionDescription 어노테이션 단 메소드 적용 + if (exceptionDescription != null) { + generateErrorCodeResponseExample(operation, exceptionDescription.value()); + } + + return operation; + }; + } + private void generateErrorCodeResponseExample( + Operation operation, SwaggerResponseDescription type) { + + ApiResponses responses = operation.getResponses(); + + Set errorCodeList = type.getErrorCodeList(); + + Map> statusWithExampleHolders = + errorCodeList.stream() + .map( + errorCode -> ExampleHolder.builder() + .holder(getSwaggerExample(errorCode)) + .code(errorCode.getHttpStatus().value()) + .name(errorCode.toString()) + .build() + ).collect(groupingBy(ExampleHolder::getCode)); + addExamplesToResponses(responses, statusWithExampleHolders); + } + + private Example getSwaggerExample(ErrorCode errorCode) { + ErrorResponse errorResponse = ErrorResponse.of(errorCode); + Example example = new Example(); + example.description(errorCode.getMessage()); + example.setValue(errorResponse); + return example; + } + + private void addExamplesToResponses( + ApiResponses responses, Map> statusWithExampleHolders) { + statusWithExampleHolders.forEach( + (status, v) -> { + Content content = new Content(); + MediaType mediaType = new MediaType(); + ApiResponse apiResponse = new ApiResponse(); + v.forEach( + exampleHolder -> { + mediaType.addExamples( + exampleHolder.getName(), exampleHolder.getHolder()); + }); + content.addMediaType("application/json", mediaType); + apiResponse.setDescription(""); + apiResponse.setContent(content); + responses.addApiResponse(status.toString(), apiResponse); + }); + } +} From fd94086f629c838c59420befc796604ea3543b72 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 05:40:02 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[docs]=20user=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20Swagger=20UI=EB=A1=9C=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 29 ++++---- .../adapter/in/web/UserCommandController.java | 42 ++++++------ .../adapter/in/web/UserQueryController.java | 67 +++++++++++++------ .../in/web/request/UserFollowRequest.java | 5 +- .../in/web/request/UserSignupRequest.java | 4 ++ .../request/UserVerifyNicknameRequest.java | 3 + 6 files changed, 98 insertions(+), 52 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index baa9b583f..551233e56 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -11,7 +11,7 @@ @Getter public enum SwaggerResponseDescription { // -// //Auth + //Auth LOGIN(new LinkedHashSet<>(Set.of( USER_NOT_FOUND ))), @@ -19,13 +19,19 @@ public enum SwaggerResponseDescription { ))), -// //User + //User USER_SIGNUP(new LinkedHashSet<>(Set.of( - ))), - USER_CHOICE_ALIAS(new LinkedHashSet<>(Set.of( + ALIAS_NAME_NOT_MATCH ))), // Follow + CHANGE_FOLLOW_STATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + USER_ALREADY_FOLLOWED, + USER_ALREADY_UNFOLLOWED, + USER_CANNOT_FOLLOW_SELF, + FOLLOW_COUNT_IS_ZERO + ))), GET_USER_FOLLOW(new LinkedHashSet<>(Set.of( USER_NOT_FOUND ))), @@ -40,15 +46,14 @@ public enum SwaggerResponseDescription { API_METHOD_NOT_ALLOWED, API_SERVER_ERROR, -// API_BAD_REQUEST, -// API_MISSING_PARAM, -// API_INVALID_PARAM, -// API_INVALID_TYPE + API_MISSING_PARAM, + API_INVALID_PARAM, + API_INVALID_TYPE - AUTH_INVALID_TOKEN, - AUTH_EXPIRED_TOKEN, - AUTH_UNAUTHORIZED, - AUTH_TOKEN_NOT_FOUND +// AUTH_INVALID_TOKEN, +// AUTH_EXPIRED_TOKEN, +// AUTH_UNAUTHORIZED, +// AUTH_TOKEN_NOT_FOUND // AUTH_LOGIN_FAILED, // AUTH_UNSUPPORTED_SOCIAL_LOGIN, diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index e7b011ef2..3d903c051 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -1,20 +1,21 @@ package konkuk.thip.user.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.Oauth2Id; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.user.adapter.in.web.request.UserFollowRequest; import konkuk.thip.user.adapter.in.web.request.UserSignupRequest; -import konkuk.thip.user.adapter.in.web.request.UserVerifyNicknameRequest; import konkuk.thip.user.adapter.in.web.response.UserFollowResponse; import konkuk.thip.user.adapter.in.web.response.UserSignupResponse; -import konkuk.thip.user.adapter.in.web.response.UserVerifyNicknameResponse; import konkuk.thip.user.application.port.in.UserFollowUsecase; import konkuk.thip.user.application.port.in.UserSignupUseCase; -import konkuk.thip.user.application.port.in.UserVerifyNicknameUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -23,22 +24,26 @@ import static konkuk.thip.common.security.constant.AuthParameters.JWT_HEADER_KEY; import static konkuk.thip.common.security.constant.AuthParameters.JWT_PREFIX; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.CHANGE_FOLLOW_STATE; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.USER_SIGNUP; +@Tag(name = "User Command API", description = "사용자가 주체가 되는 정보 수정") @RestController @RequiredArgsConstructor public class UserCommandController { private final UserSignupUseCase userSignupUseCase; - private final UserVerifyNicknameUseCase userVerifyNicknameUseCase; private final UserFollowUsecase userFollowUsecase; private final JwtUtil jwtUtil; - /** - * 사용자 회원가입 - */ + @Operation( + summary = "사용자 회원가입", + description = "사용자가 회원가입을 합니다. OAuth2 ID를 통해 사용자를 식별합니다." + ) + @ExceptionDescription(USER_SIGNUP) @PostMapping("/users/signup") public BaseResponse signup(@Valid @RequestBody final UserSignupRequest request, - @Oauth2Id final String oauth2Id, + @Parameter(hidden = true) @Oauth2Id final String oauth2Id, HttpServletResponse response) { Long userId = userSignupUseCase.signup(request.toCommand(oauth2Id)); String accessToken = jwtUtil.createAccessToken(userId); @@ -46,23 +51,20 @@ public BaseResponse signup(@Valid @RequestBody final UserSig return BaseResponse.ok(UserSignupResponse.of(userId)); } - /** - * 닉네임 중복 확인 - */ - @PostMapping("/users/nickname") - public BaseResponse verifyNickname(@Valid @RequestBody final UserVerifyNicknameRequest request) { - return BaseResponse.ok(UserVerifyNicknameResponse.of( - userVerifyNicknameUseCase.isNicknameUnique(request.nickname())) - ); - } /** * 사용자 팔로우 상태 변경 : true -> 팔로우, false -> 언팔로우 */ + @Operation( + summary = "사용자 팔로우 상태 변경", + description = "특정 사용자를 팔로우하거나 언팔로우합니다." + ) + @ExceptionDescription(CHANGE_FOLLOW_STATE) @PostMapping("/users/following/{followingUserId}") - public BaseResponse followUser(@UserId final Long userId, - @PathVariable final Long followingUserId, - @RequestBody @Valid final UserFollowRequest request) { + public BaseResponse followUser( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "팔로우/언팔로우할 사용자 ID") @PathVariable final Long followingUserId, + @RequestBody @Valid final UserFollowRequest request) { return BaseResponse.ok(UserFollowResponse.of(userFollowUsecase.changeFollowingState( UserFollowRequest.toCommand(userId, followingUserId, request.type()) ))); diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index 8a2454f17..50b3488a1 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -1,30 +1,51 @@ package konkuk.thip.user.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; +import konkuk.thip.user.adapter.in.web.request.UserVerifyNicknameRequest; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; +import konkuk.thip.user.adapter.in.web.response.UserVerifyNicknameResponse; import konkuk.thip.user.adapter.in.web.response.UserViewAliasChoiceResponse; import konkuk.thip.user.application.port.in.UserGetFollowUsecase; +import konkuk.thip.user.application.port.in.UserVerifyNicknameUseCase; import konkuk.thip.user.application.port.in.UserViewAliasChoiceUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.GET_USER_FOLLOW; + +@Tag(name = "User Query API", description = "사용자가 주체가 되는 조회") @RestController @RequiredArgsConstructor public class UserQueryController { private final UserViewAliasChoiceUseCase userViewAliasChoiceUseCase; private final UserGetFollowUsecase userGetFollowUsecase; + private final UserVerifyNicknameUseCase userVerifyNicknameUseCase; + + @Operation( + summary = "닉네임 중복 확인", + description = "사용자가 입력한 닉네임이 중복되는지 확인합니다." + ) + @PostMapping("/users/nickname") + public BaseResponse verifyNickname(@Valid @RequestBody final UserVerifyNicknameRequest request) { + return BaseResponse.ok(UserVerifyNicknameResponse.of( + userVerifyNicknameUseCase.isNicknameUnique(request.nickname())) + ); + } - /** - * 사용자 별칭 선택 화면 조회 - */ + @Operation( + summary = "사용자 별칭 선택 화면 조회", + description = "사용자가 별칭을 선택할 수 있는 화면을 조회합니다." + ) @GetMapping("/users/alias") public BaseResponse showAliasChoiceView() { return BaseResponse.ok(UserViewAliasChoiceResponse.of( @@ -32,23 +53,31 @@ public BaseResponse showAliasChoiceView() { )); } - /** - * 사용자 팔로워 조회 - */ + @Operation( + summary = "사용자 팔로워 조회", + description = "특정 사용자의 팔로워 목록을 조회합니다." + ) + @ExceptionDescription(GET_USER_FOLLOW) @GetMapping("/users/{userId}/followers") - public BaseResponse showFollowers(@PathVariable final Long userId, - @RequestParam(required = false) final String cursor, - @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { + public BaseResponse showFollowers( + @Parameter(description = "조회할 사용자 ID") @PathVariable final Long userId, + @Parameter(description = "커서") @RequestParam(required = false) final String cursor, + @Parameter(description = "단일 요청 페이지 크기 (1~10)") + @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { return BaseResponse.ok(userGetFollowUsecase.getUserFollowers(userId, cursor, size)); } - /** - * 내 팔로잉 리스트 조회 - */ + @Operation( + summary = "내 팔로잉 조회", + description = "내가 팔로우하는 사용자 목록을 조회합니다." + ) + @ExceptionDescription(GET_USER_FOLLOW) @GetMapping("/users/my/following") - public BaseResponse showMyFollowing(@UserId final Long userId, - @RequestParam(required = false) final String cursor, - @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { + public BaseResponse showMyFollowing( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "커서") @RequestParam(required = false) final String cursor, + @Parameter(description = "단일 요청 페이지 크기 (1~10)") + @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { return BaseResponse.ok(userGetFollowUsecase.getMyFollowing(userId, cursor, size)); } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java index 48f4d916d..edbe12846 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java @@ -1,11 +1,14 @@ package konkuk.thip.user.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import konkuk.thip.user.application.port.in.dto.UserFollowCommand; +@Schema(description = "사용자 팔로우 상태 변경 요청 DTO") public record UserFollowRequest( + @Schema(description = "true -> 팔로우, false -> 언팔로우", example = "true") @NotNull(message = "type은 필수 파라미터입니다.") - Boolean type // true -> 팔로우, false -> 언팔로우 + Boolean type ) { public static UserFollowCommand toCommand(Long userId, Long targetUserId, Boolean type) { return new UserFollowCommand(userId, targetUserId, type); diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java index fbe40ac17..c29515d1d 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java @@ -1,14 +1,18 @@ package konkuk.thip.user.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import konkuk.thip.user.application.port.in.dto.UserSignupCommand; +@Schema(description = "사용자 회원가입 요청 DTO") public record UserSignupRequest( + @Schema(description = "사용자 칭호", example = "문학가") @NotBlank(message = "aliasName은 필수입니다.") String aliasName, + @Schema(description = "사용자 닉네임", example = "홍길동_123") @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") @Size(max = 10, message = "닉네임은 최대 10자 입니다.") String nickname diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java index 54050a79c..733fb7cf4 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java @@ -1,9 +1,12 @@ package konkuk.thip.user.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +@Schema(description = "사용자 닉네임 중복 확인 요청 DTO") public record UserVerifyNicknameRequest( + @Schema(description = "사용자 닉네임", example = "홍길동_123") @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") @Size(max = 10, message = "닉네임은 최대 10자 입니다.") String nickname From 6585db238f463529b3f1da25a7363bc5a8abc77e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 15:22:25 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[docs]=20user=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20Swagger=20UI=EB=A1=9C=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EB=94=94=ED=85=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/user/adapter/in/web/UserCommandController.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index 3d903c051..cd984e359 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -51,13 +51,9 @@ public BaseResponse signup(@Valid @RequestBody final UserSig return BaseResponse.ok(UserSignupResponse.of(userId)); } - - /** - * 사용자 팔로우 상태 변경 : true -> 팔로우, false -> 언팔로우 - */ @Operation( summary = "사용자 팔로우 상태 변경", - description = "특정 사용자를 팔로우하거나 언팔로우합니다." + description = "특정 사용자를 팔로우하거나 언팔로우합니다. true 이면 팔로우, false 이면 언팔로우입니다." ) @ExceptionDescription(CHANGE_FOLLOW_STATE) @PostMapping("/users/following/{followingUserId}") From bcb7fe536a50b9a57528ccee3c87baa3b09e6543 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 16:42:18 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[fix]=20whilte=20list=EB=8A=94=20filter?= =?UTF-8?q?=20=EA=B1=B0=EC=B9=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/filter/JwtAuthenticationFilter.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java index abcb45aa5..3cf0920da 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -76,4 +76,16 @@ private String extractToken(HttpServletRequest request) { return null; } + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + + // 화이트리스트 경로에 대해서는 JWT 필터 제외 + return path.startsWith("/swagger-ui") + || path.startsWith("/v3/api-docs") + || path.startsWith("/api-docs") + || path.startsWith("/oauth2/authorization") + || path.startsWith("/login/oauth2/code"); + } + } From 22b2bb460882eb5b958300d3eb41f2d3fa41a30d Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 16:42:36 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[docs]=20Room=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 59 +++++++++++++++ .../adapter/in/web/RoomCommandController.java | 41 +++++++++-- .../adapter/in/web/RoomQueryController.java | 72 ++++++++++++++----- .../in/web/request/RoomCreateRequest.java | 11 +++ .../in/web/request/RoomJoinRequest.java | 3 + .../request/RoomVerifyPasswordRequest.java | 3 + 6 files changed, 164 insertions(+), 25 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 551233e56..a2d3f2290 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -36,6 +36,65 @@ public enum SwaggerResponseDescription { USER_NOT_FOUND ))), + // Room + ROOM_CREATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + BOOK_ALADIN_API_PARSING_ERROR, + BOOK_ALADIN_API_ISBN_NOT_FOUND + + ))), + ROOM_JOIN_CANCEL(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + ROOM_NOT_FOUND, + ROOM_RECRUITMENT_PERIOD_EXPIRED, + USER_CANNOT_JOIN_OR_CANCEL, + ROOM_MEMBER_COUNT_EXCEEDED, + USER_ALREADY_PARTICIPATE, + ROOM_MEMBER_COUNT_UNDERFLOW, + USER_NOT_PARTICIPATED_CANNOT_CANCEL, + HOST_CANNOT_CANCEL + ))), + ROOM_RECRUIT_CLOSE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + ROOM_NOT_FOUND, + ROOM_RECRUITMENT_PERIOD_EXPIRED, + ROOM_RECRUIT_CANNOT_CLOSED + ))), + + ROOM_SEARCH(new LinkedHashSet<>(Set.of( + INVALID_ROOM_SEARCH_SORT + ))), + ROOM_PASSWORD_CHECK(new LinkedHashSet<>(Set.of( + ROOM_NOT_FOUND, + ROOM_PASSWORD_MISMATCH, + ROOM_RECRUITMENT_PERIOD_EXPIRED, + ROOM_PASSWORD_NOT_REQUIRED + ))), + ROOM_RECRUITING_DETAIL_VIEW(new LinkedHashSet<>(Set.of( + ROOM_NOT_FOUND + ))), + ROOM_GET_HOME_JOINED_LIST(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND + ))), + ROOM_GET_MEMBER_LIST(new LinkedHashSet<>(Set.of( + ROOM_NOT_FOUND + ))), + ROOM_PLAYING_DETAIL(new LinkedHashSet<>(Set.of( + BOOK_NOT_FOUND, + ROOM_NOT_FOUND + ))), + + + // Record + RECORD_CREATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + ROOM_NOT_FOUND, + BOOK_NOT_FOUND, + ROOM_IS_EXPIRED, + RECORD_CANNOT_BE_OVERVIEW, + INVALID_RECORD_PAGE_RANGE + ))), + ; private final Set errorCodeList; diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java index e346938b4..581c054d4 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java @@ -1,8 +1,12 @@ package konkuk.thip.room.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.room.adapter.in.web.request.RoomCreateRequest; import konkuk.thip.room.adapter.in.web.request.RoomJoinRequest; import konkuk.thip.room.adapter.in.web.response.RoomCreateResponse; @@ -15,6 +19,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Room Command API", description = "방의 상태변경 API") @RestController @RequiredArgsConstructor public class RoomCommandController { @@ -26,8 +33,16 @@ public class RoomCommandController { /** * 방 생성 요청 */ + @Operation( + summary = "방 생성", + description = "사용자가 방을 생성합니다. 방 생성 시 필요한 정보를 포함한 요청을 받습니다." + ) + @ExceptionDescription(ROOM_CREATE) @PostMapping("/rooms") - public BaseResponse createRoom(@Valid @RequestBody RoomCreateRequest request, @UserId Long userId) { + public BaseResponse createRoom( + @Valid @RequestBody RoomCreateRequest request, + @Parameter(hidden = true) @UserId Long userId + ) { return BaseResponse.ok(RoomCreateResponse.of( roomCreateUseCase.createRoom(request.toCommand(), userId) )); @@ -36,11 +51,17 @@ public BaseResponse createRoom(@Valid @RequestBody RoomCreat /** * 방 참여하기/취소하기 요청 */ + @Operation( + summary = "방 참여 상태 변경", + description = "사용자가 방에 참여하거나 참여를 취소합니다. join -> 방 참여, cancel -> 방 참여 취소" + ) + @ExceptionDescription(ROOM_JOIN_CANCEL) @PostMapping("/rooms/{roomId}/join") - public BaseResponse joinRoom(@Valid @RequestBody final RoomJoinRequest request, - @UserId final Long userId, - @PathVariable final Long roomId) { - + public BaseResponse joinRoom( + @Valid @RequestBody final RoomJoinRequest request, + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "참여/취소하려는 방의 ID", example = "1") @PathVariable final Long roomId + ) { roomJoinUsecase.changeJoinState(request.toCommand(userId, roomId)); return BaseResponse.ok(null); } @@ -48,9 +69,15 @@ public BaseResponse joinRoom(@Valid @RequestBody final RoomJoinRequest req /** * 방 모집 마감하기 요청 */ + @Operation( + summary = "방 모집 마감", + description = "방장이 방의 모집을 마감합니다. 방장이 방 모집을 마감할 때 사용합니다." + ) + @ExceptionDescription(ROOM_RECRUIT_CLOSE) @PostMapping("/rooms/{roomId}/close") - public BaseResponse closeRoomRecruit(@UserId final Long userId, - @PathVariable final Long roomId) { + public BaseResponse closeRoomRecruit( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "모집을 마감할 방의 ID", example = "1") @PathVariable final Long roomId) { roomRecruitCloseUsecase.closeRoomRecruit(userId, roomId); return BaseResponse.ok(null); } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index d185ed994..198feb5ca 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -1,7 +1,11 @@ package konkuk.thip.room.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse; import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse; @@ -20,6 +24,9 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.*; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Room Query API", description = "방 조회 관련 API") @RestController @RequiredArgsConstructor public class RoomQueryController { @@ -31,53 +38,82 @@ public class RoomQueryController { private final RoomGetMemberListUseCase roomGetMemberListUseCase; private final RoomShowPlayingDetailViewUseCase roomShowPlayingDetailViewUseCase; + @Operation( + summary = "방 검색", + description = "키워드, 카테고리, 정렬 방식, 페이지 번호를 기준으로 방을 검색합니다." + ) + @ExceptionDescription(ROOM_SEARCH) @GetMapping("/rooms/search") public BaseResponse searchRooms( - @RequestParam(value = "keyword", required = false, defaultValue = "") final String keyword, - @RequestParam(value = "category", required = false, defaultValue = "") final String category, - @RequestParam("sort") final String sort, - @RequestParam("page") final int page + @Parameter(description = "검색 키워드 (책 이름 or 방 이름", example = "해리") @RequestParam(value = "keyword", required = false, defaultValue = "") final String keyword, + @Parameter(description = "모임방 카테고리", example = "문학") @RequestParam(value = "category", required = false, defaultValue = "") final String category, + @Parameter(description = "정렬 방식 (마감 임박 : deadline, 신청 인원 : memberCount)", example = "deadline") @RequestParam("sort") final String sort, + @Parameter(description = "페이지 번호", example = "1") @RequestParam("page") final int page ) { return BaseResponse.ok(roomSearchUseCase.searchRoom(keyword, category, sort, page)); } - //비공개 방 비밀번호 입력 검증 + @Operation( + summary = "비공개 방 비밀번호 입력 검증", + description = "비공개 방에 참여하기 위해 비밀번호를 검증합니다." + ) + @ExceptionDescription(ROOM_PASSWORD_CHECK) @PostMapping("/rooms/{roomId}/password") - public BaseResponse verifyRoomPassword(@PathVariable("roomId") final Long roomId, - @Valid @RequestBody final RoomVerifyPasswordRequest roomVerifyPasswordRequest - ) { + public BaseResponse verifyRoomPassword( + @Parameter(description = "비밀번호 검증하려는 비공개 방 ID", example = "1") @PathVariable("roomId") final Long roomId, + @Valid @RequestBody final RoomVerifyPasswordRequest roomVerifyPasswordRequest + ) { return BaseResponse.ok(roomVerifyPasswordUseCase.verifyRoomPassword(roomVerifyPasswordRequest.toQuery(roomId))); } - // 모집중인 방 상세보기 + @Operation( + summary = "모집중인 방 상세보기", + description = "모집중인 방의 상세 정보를 조회합니다." + ) + @ExceptionDescription(ROOM_RECRUITING_DETAIL_VIEW) @GetMapping("/rooms/{roomId}/recruiting") public BaseResponse getRecruitingRoomDetailView( - @UserId final Long userId, - @PathVariable("roomId") final Long roomId) { + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "상세보기 하려는 방의 ID", example = "1") @PathVariable("roomId") final Long roomId) { return BaseResponse.ok(roomShowRecruitingDetailViewUseCase.getRecruitingRoomDetailView(userId, roomId)); } - //[모임 홈] 참여중인 내 모임방 조회 + @Operation( + summary = "[모임 홈] 참여중인 내 모임방 조회", + description = "사용자가 참여중인 모임방 목록을 조회합니다." + ) + @ExceptionDescription(ROOM_GET_HOME_JOINED_LIST) @GetMapping("/rooms/home/joined") - public BaseResponse getHomeJoinedRooms(@UserId final Long userId, - @RequestParam("page") final int page) { + public BaseResponse getHomeJoinedRooms( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "페이지 번호", example = "1") @RequestParam("page") final int page) { return BaseResponse.ok(roomGetHomeJoinedListUseCase.getHomeJoinedRoomList( RoomGetHomeJoinedListQuery.builder() .userId(userId) .page(page).build())); } - // 독서메이트 조회 + @Operation( + summary = "독서메이트(방 참여자) 조회", + description = "특정 방의 참여자 목록을 조회합니다." + ) + @ExceptionDescription(ROOM_GET_MEMBER_LIST) @GetMapping("/rooms/{roomId}/users") - public BaseResponse getRoomMemberList(@PathVariable("roomId") final Long roomId){ + public BaseResponse getRoomMemberList( + @Parameter(description = "방 참여자 목록을 조회하려는 방의 ID", example = "1") @PathVariable("roomId") final Long roomId){ return BaseResponse.ok(roomGetMemberListUseCase.getRoomMemberList(roomId)); } // 진행중인 방 상세보기 + @Operation( + summary = "진행중인 방 상세보기", + description = "진행중인 방의 상세 정보를 조회합니다." + ) + @ExceptionDescription(ROOM_PLAYING_DETAIL) @GetMapping("/rooms/{roomId}/playing") public BaseResponse getPlayingRoomDetailView( - @UserId final Long userId, - @PathVariable("roomId") final Long roomId + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "상세보기 하려는 방의 ID", example = "1") @PathVariable("roomId") final Long roomId ) { return BaseResponse.ok(roomShowPlayingDetailViewUseCase.getPlayingRoomDetailView(userId, roomId)); } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java index fb025c30f..90a131a4a 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java @@ -1,5 +1,6 @@ package konkuk.thip.room.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import jakarta.validation.constraints.*; import konkuk.thip.room.application.port.in.dto.RoomCreateCommand; @@ -7,39 +8,49 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; +@Schema(description = "방 생성 요청 DTO") public record RoomCreateRequest( + @Schema(description = "모임방에서 기록 공유할 책의 ISBN", example = "9788936433862") @NotBlank(message = "ISBN은 필수입니다.") String isbn, + @Schema(description = "모임방 카테고리", example = "문학") @NotBlank(message = "카테고리는 필수입니다.") String category, + @Schema(description = "모임방 이름", example = "문학 모임") @NotBlank(message = "방 이름은 필수입니다.") String roomName, + @Schema(description = "모임방 설명", example = "문학을 사랑하는 사람들의 모임입니다.") @NotBlank(message = "설명은 필수입니다.") String description, + @Schema(description = "진행 시작일", example = "2023.10.01") @Pattern( regexp = "\\d{4}\\.\\d{2}\\.\\d{2}", message = "진행 시작일은 yyyy.MM.dd 형식이어야 합니다." ) String progressStartDate, + @Schema(description = "진행 종료일", example = "2023.10.31") @Pattern( regexp = "\\d{4}\\.\\d{2}\\.\\d{2}", message = "진행 종료일은 yyyy.MM.dd 형식이어야 합니다." ) String progressEndDate, + @Schema(description = "모집 인원 (1~30)", example = "5") @Min(value = 1, message = "모집 인원은 최소 1명이어야 합니다.") @Max(value = 30, message = "모집 인원은 최대 30명이어야 합니다.") int recruitCount, + @Schema(description = "방 비밀번호 (숫자 4자리)", example = "1234") @Nullable @Pattern(regexp = "\\d{4}", message = "비밀번호는 숫자 4자리여야 합니다.") String password, + @Schema(description = "방 공개 설정 여부 (true: 공개, false: 비공개)", example = "true") @NotNull(message = "방 공개 설정 여부는 필수입니다.") Boolean isPublic ) { diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomJoinRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomJoinRequest.java index baf6d3f0d..0b95ec582 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomJoinRequest.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomJoinRequest.java @@ -1,9 +1,12 @@ package konkuk.thip.room.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import konkuk.thip.room.application.port.in.dto.RoomJoinCommand; +@Schema(description = "방 참여/취소 요청 DTO") public record RoomJoinRequest( + @Schema(description = "방 참여 유형 (join: 참여, cancel: 취소)", example = "join") @NotBlank(message = "방 참여 유형 파라미터는 필수입니다.") String type ) { diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java index 153aae0e7..39acc1dbe 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java @@ -1,10 +1,13 @@ package konkuk.thip.room.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Pattern; import konkuk.thip.room.application.port.in.dto.RoomVerifyPasswordQuery; +@Schema(description = "비공개 방 비밀번호 입력 검증 요청 DTO") public record RoomVerifyPasswordRequest( + @Schema(description = "방 비밀번호 (숫자 4자리)", example = "1234") @Pattern(regexp = "\\d{4}", message = "비밀번호는 숫자 4자리여야 합니다.") String password ) { From baae8ca534ea788955aa135e35673e2ba2b4d18a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 17:01:26 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[refactor]=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BF=BC=EB=A6=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/out/persistence/PostLikeJpaRepository.java | 3 --- .../persistence/PostLikeQueryPersistenceAdapter.java | 10 ---------- .../post/application/port/out/PostLikeQueryPort.java | 3 --- 3 files changed, 16 deletions(-) diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java index 2acd4c8bb..0976e701e 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java @@ -12,9 +12,6 @@ @Repository public interface PostLikeJpaRepository extends JpaRepository { - int countByPostJpaEntity_PostId(Long postId); - - boolean existsByPostJpaEntity_PostIdAndUserJpaEntity_UserId(Long postId, Long userId); @Query(value = "SELECT pl.post_id FROM post_likes pl WHERE pl.user_id = :userId AND pl.post_id IN (:postIds)", nativeQuery = true) Set findPostIdsLikedByUser(@Param("postIds") Set postIds, diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java index 49e8973d3..96d428325 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java @@ -14,16 +14,6 @@ public class PostLikeQueryPersistenceAdapter implements PostLikeQueryPort { private final PostLikeJpaRepository postLikeJpaRepository; - @Override - public int countByPostId(Long postId) { - return postLikeJpaRepository.countByPostJpaEntity_PostId(postId); - } - - @Override - public boolean existsByPostIdAndUserId(Long postId, Long userId) { - return postLikeJpaRepository.existsByPostJpaEntity_PostIdAndUserJpaEntity_UserId(postId, userId); - } - @Override public Set findPostIdsLikedByUser(Set postIds, Long userId) { return postLikeJpaRepository.findPostIdsLikedByUser(postIds, userId); diff --git a/src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java b/src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java index 40448b4b7..dc58912a8 100644 --- a/src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java +++ b/src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java @@ -6,9 +6,6 @@ public interface PostLikeQueryPort { - int countByPostId(Long postId); - boolean existsByPostIdAndUserId(Long postId, Long userId); - List findLikedFeedIdsByUserIdAndFeedIds(Long userId, List feedIds); Set findPostIdsLikedByUser(Set postIds, Long userId); } From f59b4967e391c47db762b76742da88c916086ac9 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 17:40:59 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[docs]=20Vote=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/swagger/SwaggerResponseDescription.java | 10 ++++++++++ .../vote/adapter/in/web/VoteCommandController.java | 12 ++++++++++-- .../vote/adapter/in/web/VoteQueryController.java | 2 ++ .../adapter/in/web/request/VoteCreateRequest.java | 9 +++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index a2d3f2290..4372c95c9 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -95,6 +95,16 @@ public enum SwaggerResponseDescription { INVALID_RECORD_PAGE_RANGE ))), + // Vote + VOTE_CREATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + ROOM_NOT_FOUND, + BOOK_NOT_FOUND, + ROOM_IS_EXPIRED, + VOTE_CANNOT_BE_OVERVIEW, + INVALID_VOTE_PAGE_RANGE + ))), + ; private final Set errorCodeList; diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java b/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java index 2ed5025d5..b28ebb890 100644 --- a/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java @@ -1,5 +1,8 @@ package konkuk.thip.vote.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; @@ -12,16 +15,21 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Vote Command API", description = "투표 상태변경 관련 API") @RestController @RequiredArgsConstructor public class VoteCommandController { private final VoteCreateUseCase voteCreateUseCase; + @Operation( + summary = "투표 생성", + description = "방에 대한 투표를 생성합니다." + ) @PostMapping("/rooms/{roomId}/vote") public BaseResponse createVote( - @UserId Long userId, - @PathVariable Long roomId, + @Parameter(hidden = true) @UserId Long userId, + @Parameter(description = "투표를 생성할 방 ID", example = "1") @PathVariable Long roomId, @Valid @RequestBody VoteCreateRequest request) { return BaseResponse.ok(VoteCreateResponse.of( diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/VoteQueryController.java b/src/main/java/konkuk/thip/vote/adapter/in/web/VoteQueryController.java index ca617d8c9..2557f95fb 100644 --- a/src/main/java/konkuk/thip/vote/adapter/in/web/VoteQueryController.java +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/VoteQueryController.java @@ -1,8 +1,10 @@ package konkuk.thip.vote.adapter.in.web; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Vote Query API", description = "투표 조회 관련 API") @RestController @RequiredArgsConstructor public class VoteQueryController { diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java b/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java index 425f51517..f9e720f7d 100644 --- a/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java @@ -1,5 +1,6 @@ package konkuk.thip.vote.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -8,23 +9,31 @@ import java.util.List; +@Schema(description = "투표 생성 요청 DTO") public record VoteCreateRequest( + @Schema(description = "투표를 생성할 책의 페이지 번호", example = "20") @NotNull(message = "page는 필수입니다.") Integer page, + @Schema(description = "총평 여부", example = "true") @NotNull(message = "isOverview(= 총평 여부)는 필수입니다.") Boolean isOverview, + @Schema(description = "투표 내용", example = "띱은 최고의 서비스인가?") @NotBlank(message = "투표 내용은 필수입니다.") @Size(max = 20, message = "투표 내용은 최대 20자 입니다.") String content, + @Schema(description = "투표 항목 리스트", example = "[{\"itemName\": \"네\"}, {\"itemName\": \"아니오\"}]") @NotNull(message = "투표 항목은 필수입니다.") @Size(min = 1, max = 5, message = "투표 항목은 1개 이상, 최대 5개까지입니다.") @Valid List voteItemList ) { + @Schema(description = "투표 항목 DTO") public record VoteItemCreateRequest( + + @Schema(description = "투표 항목 이름", example = "네") @NotBlank(message = "투표 항목 이름은 필수입니다.") @Size(max = 20, message = "투표 항목 이름은 최대 20자입니다.") String itemName From 8242dbba9b46e76c94db58afbf339ac55fe954b9 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 17:52:39 +0900 Subject: [PATCH 09/19] =?UTF-8?q?[refactor]=20=EC=95=88=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/record/application/service/RecordSearchService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java b/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java index 67ea21dac..ce4dfc2f6 100644 --- a/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java +++ b/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java @@ -78,7 +78,7 @@ public RecordSearchResponse search(RecordSearchQuery recordSearchQuery) { pageEnd = book.getPageCount(); } } - yield getGroupRecordBySortParams(recordSearchQuery.sort(), roomId, userId, cursor, pageStart, pageEnd, isPageFilter, isOverview); + yield getGroupRecordBySortParams(recordSearchQuery.sort(), roomId, userId, cursor, pageStart, pageEnd, isOverview); } case MINE -> { validateMyRecordFilters(pageStart, pageEnd, isPageFilter, isOverview, recordSearchQuery.sort()); @@ -110,7 +110,7 @@ public RecordSearchResponse search(RecordSearchQuery recordSearchQuery) { .build(); } - private CursorBasedList getGroupRecordBySortParams(String sort, Long roomId, Long userId, Cursor cursor, Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview) { + private CursorBasedList getGroupRecordBySortParams(String sort, Long roomId, Long userId, Cursor cursor, Integer pageStart, Integer pageEnd, Boolean isOverview) { return switch(RecordSearchSortParams.from(sort)) { case LATEST -> recordQueryPort.searchGroupRecordsByLatest(roomId, userId, cursor, pageStart, pageEnd, isOverview); case LIKE -> recordQueryPort.searchGroupRecordsByLike(roomId, userId, cursor, pageStart, pageEnd, isOverview); From 8ca4cd1511c60cbfc4a2988fbfedbe45ba3a98de Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 17:57:29 +0900 Subject: [PATCH 10/19] =?UTF-8?q?[docs]=20Record=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 6 ++++ .../in/web/RecordCommandController.java | 24 ++++++++++--- .../adapter/in/web/RecordQueryController.java | 35 +++++++++++-------- .../in/web/request/RecordCreateRequest.java | 21 +++++++---- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 4372c95c9..b2ef85b49 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -94,6 +94,12 @@ public enum SwaggerResponseDescription { RECORD_CANNOT_BE_OVERVIEW, INVALID_RECORD_PAGE_RANGE ))), + RECORD_SEARCH(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + ROOM_NOT_FOUND, + BOOK_NOT_FOUND, + USER_NOT_BELONG_TO_ROOM + ))), // Vote VOTE_CREATE(new LinkedHashSet<>(Set.of( diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java b/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java index c041b3737..dfca9e812 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java @@ -1,24 +1,40 @@ package konkuk.thip.record.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.record.adapter.in.web.request.RecordCreateRequest; import konkuk.thip.record.adapter.in.web.response.RecordCreateResponse; import konkuk.thip.record.application.port.in.RecordCreateUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Record Command API", description = "기록 상태변경 관련 API") @RestController @RequiredArgsConstructor public class RecordCommandController { private final RecordCreateUseCase recordCreateUseCase; + @Operation( + summary = "기록 생성", + description = "방에 대한 기록을 생성합니다." + ) + @ExceptionDescription(RECORD_CREATE) @PostMapping("/rooms/{roomId}/record") public BaseResponse createRecord( - @UserId final Long userId, - @PathVariable final Long roomId, - @Valid @RequestBody final RecordCreateRequest recordCreateRequest) { + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "기록을 생성할 방 ID", example = "1") @PathVariable final Long roomId, + @Valid @RequestBody final RecordCreateRequest recordCreateRequest + ) { return BaseResponse.ok( RecordCreateResponse.of( recordCreateUseCase.createRecord(recordCreateRequest.toCommand(roomId, userId)) diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java b/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java index 1b117774c..4408f1dee 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java @@ -1,7 +1,11 @@ package konkuk.thip.record.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; import konkuk.thip.record.application.port.in.dto.RecordSearchQuery; import konkuk.thip.record.application.port.in.dto.RecordSearchUseCase; @@ -11,35 +15,38 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.RECORD_SEARCH; + +@Tag(name = "Record Query API", description = "기록 조회 관련 API") @RestController @RequiredArgsConstructor public class RecordQueryController { private final RecordSearchUseCase recordSearchUseCase; - /** - * 방의 게시글(기록, 투표) 목록 조회 - * @param roomId - * @param type : group , mine - * @param sort : 그룹 기록 -> 최신순 / 내 기록 -> 페이지 높은 순 default 정렬 - * @param pageStart - * @param pageEnd - * @param isOverview : 총평보기 필터 여부 - * @param isPageFilter : 페이지 필터 여부 - * @param userId - * @return - */ + @Operation( + summary = "방의 게시글(기록, 투표) 목록 조회", + description = "방의 게시글(기록, 투표) 목록을 조회합니다. type에 따라 그룹 기록 또는 내 기록을 조회할 수 있습니다." + ) + @ExceptionDescription(RECORD_SEARCH) @GetMapping("/rooms/{roomId}/posts") public BaseResponse viewRecordList( - @PathVariable final Long roomId, + @Parameter(description = "게시글을 조회할 방 ID", example = "1") @PathVariable final Long roomId, + @Parameter(description = "게시글 조회 타입 (group: 그룹 기록, mine: 내 기록)", example = "group") @RequestParam(required = false, defaultValue = "group") final String type, + @Parameter(description = "게시글 정렬 기준 (최신순: latest, 인기 순: like, 댓글 많은 순: comment) \n그룹 기록에서만 사용됩니다. 내 기록에서는 페이지 높은 순 고정 정렬", example = "latest") @RequestParam(required = false) final String sort, + @Parameter(description = "게시글 페이지 필터링 시작 범위 (default: 0)", example = "10") @RequestParam(required = false) final Integer pageStart, + @Parameter(description = "게시글 페이지 필터링 종료 범위 (default: 책의 마지막 페이지)", example = "100") @RequestParam(required = false) final Integer pageEnd, + @Parameter(description = "총평 보기 필터 여부 (default: false)", example = "true") @RequestParam(required = false, defaultValue = "false") final Boolean isOverview, + @Parameter(description = "페이지 필터 여부 (default: false)", example = "true") @RequestParam(required = false, defaultValue = "false") final Boolean isPageFilter, + @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(required = false) final String cursor, - @UserId final Long userId + @Parameter(hidden = true) @UserId final Long userId ) { return BaseResponse.ok(recordSearchUseCase.search( RecordSearchQuery.builder() diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java b/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java index 856061bd0..d0086eb84 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java @@ -1,20 +1,27 @@ package konkuk.thip.record.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import konkuk.thip.record.application.port.in.dto.RecordCreateCommand; +@Schema( + description = "기록 생성 요청 DTO" +) public record RecordCreateRequest ( - @NotNull(message = "page는 필수입니다.") - Integer page, + @Schema(description = "기록을 생성할 책의 페이지 번호", example = "20") + @NotNull(message = "page는 필수입니다.") + Integer page, - @NotNull(message = "isOverview(= 총평 여부)는 필수입니다.") - Boolean isOverview, + @Schema(description = "총평 여부", example = "true") + @NotNull(message = "isOverview(= 총평 여부)는 필수입니다.") + Boolean isOverview, - @NotBlank(message = "기록 내용은 필수입니다.") - @Size(max = 500, message = "기록 내용은 최대 500자 입니다.") - String content + @Schema(description = "기록 내용", example = "띱은 최고의 서비스인 것 같습니다.") + @NotBlank(message = "기록 내용은 필수입니다.") + @Size(max = 500, message = "기록 내용은 최대 500자 입니다.") + String content ) { public RecordCreateCommand toCommand(Long roomId, Long creatorId) { return new RecordCreateCommand( From 7d3d4ca0179bdce8534e5e421b91bc35163c22c6 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 18:17:31 +0900 Subject: [PATCH 11/19] =?UTF-8?q?[docs]=20Feed=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 30 ++++++++++++ .../adapter/in/web/FeedCommandController.java | 48 +++++++++++++------ .../adapter/in/web/FeedQueryController.java | 15 +++++- .../in/web/request/FeedCreateRequest.java | 6 +++ .../in/web/request/FeedIsSavedRequest.java | 3 ++ .../adapter/in/web/RoomCommandController.java | 2 +- 6 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index b2ef85b49..dc2f247c4 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -111,6 +111,36 @@ public enum SwaggerResponseDescription { INVALID_VOTE_PAGE_RANGE ))), + // FEED + FEED_CREATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + BOOK_NOT_FOUND, + TAG_NOT_FOUND, + TAG_NAME_NOT_MATCH, + INVALID_FEED_COMMAND, + BOOK_NAVER_API_PARSING_ERROR, + BOOK_NAVER_API_ISBN_NOT_FOUND, + EMPTY_FILE_EXCEPTION, + EXCEPTION_ON_IMAGE_UPLOAD, + INVALID_FILE_EXTENSION + ))), + FEED_UPDATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + FEED_NOT_FOUND, + BOOK_NOT_FOUND, + TAG_NOT_FOUND, + TAG_NAME_NOT_MATCH, + INVALID_FEED_COMMAND, + FEED_ACCESS_FORBIDDEN + ))), + CHANGE_FEED_SAVED_STATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + FEED_NOT_FOUND, + FEED_ALREADY_SAVED, + FEED_NOT_SAVED_CANNOT_DELETE + ))), + FEED_SEARCH(new LinkedHashSet<>(Set.of())), + ; private final Set errorCodeList; diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java index c43488005..0513d0eae 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java @@ -1,8 +1,12 @@ package konkuk.thip.feed.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.feed.adapter.in.web.request.FeedCreateRequest; import konkuk.thip.feed.adapter.in.web.request.FeedIsSavedRequest; import konkuk.thip.feed.adapter.in.web.request.FeedUpdateRequest; @@ -12,13 +16,14 @@ import konkuk.thip.feed.application.port.in.FeedSavedUseCase; import konkuk.thip.feed.application.port.in.FeedUpdateUseCase; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; -@Slf4j +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Feed Command API", description = "피드 상태변경 관련 API") @RestController @RequiredArgsConstructor public class FeedCommandController { @@ -27,29 +32,44 @@ public class FeedCommandController { private final FeedUpdateUseCase feedUpdateUseCase; private final FeedSavedUseCase feedSavedUseCase; - //피드 작성 + @Operation( + summary = "피드 작성", + description = "사용자가 피드를 작성합니다." + ) + @ExceptionDescription(FEED_CREATE) @PostMapping("/feeds") - public BaseResponse createFeed(@RequestPart("request") @Valid final FeedCreateRequest request, - @RequestPart(value = "images", required = false) final List images, - @UserId final Long userId) { + public BaseResponse createFeed( + @RequestPart("request") @Valid final FeedCreateRequest request, + @Parameter(description = "피드에 첨부할 이미지 파일들") @RequestPart(value = "images", required = false) final List images, + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(FeedIdResponse.of(feedCreateUseCase.createFeed(request.toCommand(userId),images))); } - // 피드 수정 (책 빼고 변경가능) + @Operation( + summary = "피드 수정 (책 빼고 변경가능)", + description = "사용자가 피드를 수정합니다." + ) + @ExceptionDescription(FEED_UPDATE) @PatchMapping("/feeds/{feedId}") - public BaseResponse updateFeed(@RequestBody @Valid final FeedUpdateRequest request, - @PathVariable("feedId") final Long feedId, - @UserId final Long userId) { + public BaseResponse updateFeed( + @RequestBody @Valid final FeedUpdateRequest request, + @Parameter(description = "수정할 피드 ID") @PathVariable("feedId") final Long feedId, + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(FeedIdResponse.of(feedUpdateUseCase.updateFeed(request.toCommand(userId,feedId)))); } - //피드 저장상태 변경: true -> 저장, false -> 저장해제(삭제) + @Operation( + summary = "피드 저장 상태 변경", + description = "사용자가 피드의 저장 상태를 변경합니다. 저장: true, 저장해제(삭제): false" + ) + @ExceptionDescription(CHANGE_FEED_SAVED_STATE) @PostMapping("/feeds/{feedId}/saved") - public BaseResponse changeSavedFeed(@RequestBody final FeedIsSavedRequest request, - @PathVariable("feedId") final Long feedId, - @UserId final Long userId) { + public BaseResponse changeSavedFeed( + @RequestBody final FeedIsSavedRequest request, + @Parameter(description = "저장 상태 변경하려는 피드 ID") @PathVariable("feedId") final Long feedId, + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(FeedIsSavedResponse.of(feedSavedUseCase.changeSavedFeed(FeedIsSavedRequest.toCommand(userId,feedId,request.type())))); } diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java index 405c65aef..4707df1f3 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java @@ -1,7 +1,11 @@ package konkuk.thip.feed.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.feed.adapter.in.web.response.FeedShowAllResponse; import konkuk.thip.feed.application.port.in.FeedShowAllUseCase; import lombok.RequiredArgsConstructor; @@ -9,15 +13,24 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.FEED_SEARCH; + +@Tag(name = "Feed Query API", description = "피드 조회 관련 API") @RestController @RequiredArgsConstructor public class FeedQueryController { private final FeedShowAllUseCase feedShowAllUseCase; + @Operation( + summary = "피드 전체 조회", + description = "사용자가 작성한 피드를 전체 조회합니다." + ) + @ExceptionDescription(FEED_SEARCH) @GetMapping("/feeds") public BaseResponse showAllFeeds( - @UserId final Long userId, + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor) { return BaseResponse.ok(feedShowAllUseCase.showAllFeeds(userId, cursor)); } diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java index a4a518638..960d7174b 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java @@ -1,22 +1,28 @@ package konkuk.thip.feed.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import konkuk.thip.feed.application.port.in.dto.FeedCreateCommand; import java.util.List; +@Schema(description = "피드 생성 요청 DTO") public record FeedCreateRequest( + @Schema(description = "생성할 피드의 책 ISBN", example = "9788936433862") @NotBlank(message = "ISBN은 필수입니다.") String isbn, + @Schema(description = "피드 내용", example = "이 책은 정말 좋습니다!") @NotBlank(message = "콘텐츠 내용은 필수입니다.") String contentBody, + @Schema(description = "방 공개 설정 여부 (true: 공개, false: 비공개)", example = "true") @NotNull(message = "방 공개 설정 여부는 필수입니다.") Boolean isPublic, + @Schema(description = "피드에 추가할 태그들", example = "[\"한국소설\", \"외국소설\", \"시\"]") List tagList ) { public FeedCreateCommand toCommand(Long userId) { diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java index fb0f290e0..8615e809e 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java @@ -1,9 +1,12 @@ package konkuk.thip.feed.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import konkuk.thip.feed.application.port.in.dto.FeedIsSavedCommand; +@Schema(description = "피드 저장 상태 요청 DTO") public record FeedIsSavedRequest( + @Schema(description = "저장 상태 종류 (저장하기: true, 저장취소: false)", example = "true") @NotNull(message = "type은 필수입니다.") Boolean type ) { diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java index 581c054d4..2624313d3 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java @@ -21,7 +21,7 @@ import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; -@Tag(name = "Room Command API", description = "방의 상태변경 API") +@Tag(name = "Room Command API", description = "방 상태변경 관련 API") @RestController @RequiredArgsConstructor public class RoomCommandController { From 1bc668ddd04a2f889621c1dda7747c9efb7d3c8a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 18:17:51 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[docs]=20Feed=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/swagger/SwaggerResponseDescription.java | 1 - .../konkuk/thip/feed/adapter/in/web/FeedQueryController.java | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index dc2f247c4..f38bdf53b 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -139,7 +139,6 @@ public enum SwaggerResponseDescription { FEED_ALREADY_SAVED, FEED_NOT_SAVED_CANNOT_DELETE ))), - FEED_SEARCH(new LinkedHashSet<>(Set.of())), ; diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java index 4707df1f3..239b5154c 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; -import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.feed.adapter.in.web.response.FeedShowAllResponse; import konkuk.thip.feed.application.port.in.FeedShowAllUseCase; import lombok.RequiredArgsConstructor; @@ -13,8 +12,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import static konkuk.thip.common.swagger.SwaggerResponseDescription.FEED_SEARCH; - @Tag(name = "Feed Query API", description = "피드 조회 관련 API") @RestController @RequiredArgsConstructor @@ -26,7 +23,6 @@ public class FeedQueryController { summary = "피드 전체 조회", description = "사용자가 작성한 피드를 전체 조회합니다." ) - @ExceptionDescription(FEED_SEARCH) @GetMapping("/feeds") public BaseResponse showAllFeeds( @Parameter(hidden = true) @UserId final Long userId, From cf40d6c7105e9fdbfdcc4073316f2423434a83d3 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 18:28:35 +0900 Subject: [PATCH 13/19] =?UTF-8?q?[docs]=20Comment=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/CommentCommandController.java | 40 +++++++++++++++---- .../in/web/request/CommentCreateRequest.java | 6 +++ .../in/web/request/CommentIsLikeRequest.java | 3 ++ .../swagger/SwaggerResponseDescription.java | 23 +++++++++++ 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java index 8898571f1..492856503 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java @@ -1,5 +1,8 @@ package konkuk.thip.comment.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import konkuk.thip.comment.adapter.in.web.request.CommentCreateRequest; import konkuk.thip.comment.adapter.in.web.request.CommentIsLikeRequest; @@ -9,9 +12,17 @@ import konkuk.thip.comment.application.port.in.CommentLikeUseCase; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.CHANGE_COMMENT_LIKE_STATE; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.COMMENT_CREATE; + +@Tag(name = "Comment Command API", description = "댓글 상태변경 관련 API") @RestController @RequiredArgsConstructor public class CommentCommandController { @@ -24,18 +35,31 @@ public class CommentCommandController { * parentId:{Long},isReplyRequest:true 답글 * parentId:null,isReplyRequest:false 댓글 */ + @Operation( + summary = "댓글 작성", + description = "사용자가 댓글을 작성합니다.\n" + + "답글 작성 시 parentId를 지정하고 isReplyRequest를 true로 설정합니다. " + + "댓글 작성 시 parentId는 null로 설정하고 isReplyRequest를 false로 설정합니다." + ) + @ExceptionDescription(COMMENT_CREATE) @PostMapping("/comments/{postId}") - public BaseResponse createComment(@RequestBody @Valid final CommentCreateRequest request, - @PathVariable("postId") final Long postId, - @UserId final Long userId) { + public BaseResponse createComment( + @RequestBody @Valid final CommentCreateRequest request, + @Parameter(description = "댓글을 작성하려는 게시물 ID", example = "1") @PathVariable("postId") final Long postId, + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(CommentIdResponse.of(commentCreateUseCase.createComment(request.toCommand(userId,postId)))); } - //댓글 좋아요 상태 변경: true -> 좋아요, false -> 좋아요 취소 + @Operation( + summary = "댓글 좋아요 상태 변경", + description = "사용자가 댓글의 좋아요 상태를 변경합니다. (true -> 좋아요, false -> 좋아요 취소)" + ) + @ExceptionDescription(CHANGE_COMMENT_LIKE_STATE) @PostMapping("/comments/{commentId}/likes") - public BaseResponse likeComment(@RequestBody @Valid final CommentIsLikeRequest request, - @PathVariable("commentId") final Long commentId, - @UserId final Long userId) { + public BaseResponse likeComment( + @RequestBody @Valid final CommentIsLikeRequest request, + @Parameter(description = "좋아요 상태를 변경하려는 댓글 ID", example = "1") @PathVariable("commentId") final Long commentId, + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(CommentIsLikeResponse.of(commentLikeUseCase.changeLikeStatusComment(request.toCommand(userId, commentId)))); } diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentCreateRequest.java b/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentCreateRequest.java index dbc5abc6f..976864530 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentCreateRequest.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentCreateRequest.java @@ -1,19 +1,25 @@ package konkuk.thip.comment.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import konkuk.thip.comment.application.port.in.dto.CommentCreateCommand; +@Schema(description = "댓글 작성 요청 DTO") public record CommentCreateRequest( + @Schema(description = "댓글 내용", example = "이 게시물 정말 좋아요!") @NotBlank(message = "댓글 내용은 필수입니다.") String content, + @Schema(description = "답글 여부", example = "true") @NotNull(message = "답글 여부는 필수입니다.") Boolean isReplyRequest, + @Schema(description = "답글의 부모 댓글 ID (답글이 아닐 경우 null)", example = "1") Long parentId, + @Schema(description = "게시물 타입 (RECORD, VOTE, FEED)", example = "RECORD") @NotBlank(message = "게시물 타입은 필수입니다.") String postType diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentIsLikeRequest.java b/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentIsLikeRequest.java index 72da84898..a14d9b4c7 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentIsLikeRequest.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentIsLikeRequest.java @@ -1,9 +1,12 @@ package konkuk.thip.comment.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import konkuk.thip.comment.application.port.in.dto.CommentIsLikeCommand; +@Schema(description = "댓글 좋아요 상태 변경 요청 DTO") public record CommentIsLikeRequest( + @Schema(description = "좋아요 여부 type (true -> 좋아요, false -> 좋아요 취소)", example = "true") @NotNull(message = "좋아요 여부는 필수입니다.") Boolean type ) { diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index f38bdf53b..47bd86844 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -140,6 +140,29 @@ public enum SwaggerResponseDescription { FEED_NOT_SAVED_CANNOT_DELETE ))), + // Comment + COMMENT_CREATE(new LinkedHashSet<>(Set.of( + POST_TYPE_NOT_MATCH, + USER_NOT_FOUND, + FEED_NOT_FOUND, + RECORD_NOT_FOUND, + VOTE_NOT_FOUND, + INVALID_COMMENT_CREATE, + FEED_ACCESS_FORBIDDEN, + ROOM_ACCESS_FORBIDDEN + + ))), + CHANGE_COMMENT_LIKE_STATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + COMMENT_NOT_FOUND, + FEED_NOT_FOUND, + RECORD_NOT_FOUND, + VOTE_NOT_FOUND, + COMMENT_ALREADY_LIKED, + COMMENT_NOT_LIKED_CANNOT_CANCEL, + COMMENT_LIKE_COUNT_UNDERFLOW + ))), + ; private final Set errorCodeList; From 61d1895cee0c00f7adb087cc8d3527717e756894 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 18:46:31 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[docs]=20Book=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EB=AA=85=EC=84=B8=20(#1?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/BookCommandController.java | 29 +++++++--- .../adapter/in/web/BookQueryController.java | 53 +++++++++++++------ .../web/request/PostBookIsSavedRequest.java | 4 +- .../thip/common/exception/code/ErrorCode.java | 8 +-- .../swagger/SwaggerResponseDescription.java | 27 ++++++++++ 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java index 9039a4fe2..63cf156a8 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java @@ -1,15 +1,26 @@ package konkuk.thip.book.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; import konkuk.thip.book.adapter.in.web.response.PostBookIsSavedResponse; import konkuk.thip.book.application.port.in.BookSavedUseCase; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.CHANGE_BOOK_SAVED_STATE; + +@Tag(name = "Book Command API", description = "책 상태변경 관련 API") @Validated @RestController @RequiredArgsConstructor @@ -17,12 +28,18 @@ public class BookCommandController { private final BookSavedUseCase bookSavedUseCase; - //책 저장 상태 변경 + @Operation( + summary = "책 저장 상태 변경", + description = "사용자가 책의 저장 상태를 변경합니다. (true -> 저장, false -> 저장 취소)" + ) + @ExceptionDescription(CHANGE_BOOK_SAVED_STATE) @PostMapping("/books/{isbn}/saved") - public BaseResponse changeSavedBook(@PathVariable("isbn") - @Pattern(regexp = "\\d{13}") final String isbn, - @RequestBody final PostBookIsSavedRequest postBookIsSavedRequest, - @UserId final Long userId) { + public BaseResponse changeSavedBook( + @Parameter(description = "책의 ISBN 번호 (13자리 숫자)", example = "9781234567890") + @PathVariable("isbn") @Pattern(regexp = "\\d{13}") final String isbn, + @RequestBody @Valid final PostBookIsSavedRequest postBookIsSavedRequest, + @Parameter(hidden = true) @UserId final Long userId + ) { return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.changeSavedBook(isbn,postBookIsSavedRequest.type(),userId))); } diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java b/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java index b1e7de2a9..bf3a80380 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java @@ -1,17 +1,27 @@ package konkuk.thip.book.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Pattern; import konkuk.thip.book.adapter.in.web.response.GetBookDetailSearchResponse; import konkuk.thip.book.adapter.in.web.response.GetBookMostSearchResponse; import konkuk.thip.book.adapter.in.web.response.GetBookSearchListResponse; -import konkuk.thip.book.application.port.in.BookSearchUseCase; import konkuk.thip.book.application.port.in.BookMostSearchUseCase; +import konkuk.thip.book.application.port.in.BookSearchUseCase; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Book Query API", description = "책 조회 관련 API") @Validated @RestController @RequiredArgsConstructor @@ -20,30 +30,43 @@ public class BookQueryController { private final BookSearchUseCase bookSearchUseCase; private final BookMostSearchUseCase bookMostSearchUseCase; - - //책 검색결과 조회 + @Operation( + summary = "책 검색결과 조회", + description = "사용자가 입력한 키워드로 책을 검색합니다." + ) + @ExceptionDescription(BOOK_SEARCH) @GetMapping("/books") - public BaseResponse getBookSearchList(@RequestParam final String keyword, - @RequestParam final int page, - @UserId final Long userId) { + public BaseResponse getBookSearchList( + @Parameter(description = "검색 키워드", example = "해리포터") @RequestParam final String keyword, + @Parameter(description = "페이지 번호 (1부터 시작)", example = "1") @RequestParam final int page, + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(GetBookSearchListResponse.of(bookSearchUseCase.searchBooks(keyword, page,userId), page)); } //책 상세검색 결과 조회 + @Operation( + summary = "책 상세검색 결과 조회", + description = "ISBN을 통해 책의 상세 정보를 조회합니다." + ) + @ExceptionDescription(BOOK_DETAIL_SEARCH) @GetMapping("/books/{isbn}") - public BaseResponse getBookDetailSearch(@PathVariable("isbn") - @Pattern(regexp = "\\d{13}") final String isbn, - @UserId final Long userId) { - - - + public BaseResponse getBookDetailSearch( + @Parameter(description = "책의 ISBN 번호 (13자리 숫자)", example = "9781234567890") + @PathVariable("isbn") @Pattern(regexp = "\\d{13}") final String isbn, + @Parameter(hidden = true) @UserId final Long userId + ) { return BaseResponse.ok(GetBookDetailSearchResponse.of(bookSearchUseCase.searchDetailBooks(isbn,userId))); } //가장 많이 검색된 책 조회 + @Operation( + summary = "가장 많이 검색된 책 조회", + description = "사용자가 가장 많이 검색한 책들을 조회합니다." + ) + @ExceptionDescription(POPULAR_BOOK_SEARCH) @GetMapping("/books/most-searched") - public BaseResponse getMostSearchedBooks(@UserId final Long userId) { - + public BaseResponse getMostSearchedBooks( + @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(GetBookMostSearchResponse.of(bookMostSearchUseCase.getMostSearchedBooks(userId))); } diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java b/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java index ba7b7b9bf..1c5654230 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java @@ -1,9 +1,11 @@ package konkuk.thip.book.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; - +@Schema(description = "책 저장 상태 변경 요청 DTO") public record PostBookIsSavedRequest( + @Schema(description = "저장 여부 type (true -> 저장, false -> 저장 취소)", example = "true") @NotNull(message = "type은 필수입니다.") boolean type ) { diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index c2c54d8ac..421bed0da 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -50,11 +50,11 @@ public enum ErrorCode implements ResponseCode { /** * 80000 : book error */ - BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.BAD_REQUEST, 80000, "검색어 인코딩에 실패했습니다."), - BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001,"네이버 API 요청에 실패하였습니다."), + BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.INSUFFICIENT_STORAGE, 80000, "검색어 인코딩에 실패했습니다."), + BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80001,"네이버 API 요청에 실패하였습니다."), BOOK_NAVER_API_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80002,"네이버 API 응답 파싱에 실패하였습니다."), - BOOK_NAVER_API_URL_ERROR(HttpStatus.BAD_REQUEST, 80003,"네이버 API URL이 잘못되었습니다."), - BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_REQUEST, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."), + BOOK_NAVER_API_URL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80003,"네이버 API URL이 잘못되었습니다."), + BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."), BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80005,"네이버 API 응답에 실패하였습니다."), BOOK_SEARCH_PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, 80006,"검색어 페이지가 범위를 벗어났습니다."), BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 47bd86844..2dc99468b 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -163,6 +163,33 @@ public enum SwaggerResponseDescription { COMMENT_LIKE_COUNT_UNDERFLOW ))), + // Book + CHANGE_BOOK_SAVED_STATE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + BOOK_NOT_FOUND, + BOOK_ALREADY_SAVED, + BOOK_NOT_SAVED_CANNOT_DELETE +// DUPLICATED_BOOKS_IN_COLLECTION, +// BOOK_NOT_SAVED_DB_CANNOT_DELETE + ))), + BOOK_SEARCH(new LinkedHashSet<>(Set.of( + BOOK_SEARCH_PAGE_OUT_OF_RANGE, + BOOK_KEYWORD_REQUIRED, + BOOK_PAGE_NUMBER_INVALID, + BOOK_NAVER_API_PARSING_ERROR, + BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED, + BOOK_NAVER_API_RESPONSE_ERROR + ))), + BOOK_DETAIL_SEARCH(new LinkedHashSet<>(Set.of( + BOOK_NOT_FOUND, + BOOK_NAVER_API_PARSING_ERROR, + BOOK_NAVER_API_ISBN_NOT_FOUND, + BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED + ))), + POPULAR_BOOK_SEARCH(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + JSON_PROCESSING_ERROR + ))), ; private final Set errorCodeList; From b6d5b4fe870242a49a379aa74e80ac81167e52aa Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 19:03:12 +0900 Subject: [PATCH 15/19] =?UTF-8?q?[fix]=20=EC=9E=98=EB=AA=BB=EB=90=9C=20Htt?= =?UTF-8?q?pStatus=20=EC=88=98=EC=A0=95=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 421bed0da..b719506b9 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -50,7 +50,7 @@ public enum ErrorCode implements ResponseCode { /** * 80000 : book error */ - BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.INSUFFICIENT_STORAGE, 80000, "검색어 인코딩에 실패했습니다."), + BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 80000, "검색어 인코딩에 실패했습니다."), BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80001,"네이버 API 요청에 실패하였습니다."), BOOK_NAVER_API_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80002,"네이버 API 응답 파싱에 실패하였습니다."), BOOK_NAVER_API_URL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80003,"네이버 API URL이 잘못되었습니다."), From dc1271ab308ea062b994cca9ff342e506f9a72d6 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 28 Jul 2025 19:03:35 +0900 Subject: [PATCH 16/19] =?UTF-8?q?[fix]=20Request=20Dto=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Boolean=20=EB=9E=98=ED=8D=BC=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/adapter/in/web/request/PostBookIsSavedRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java b/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java index 1c5654230..f08bd9796 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java @@ -7,6 +7,6 @@ public record PostBookIsSavedRequest( @Schema(description = "저장 여부 type (true -> 저장, false -> 저장 취소)", example = "true") @NotNull(message = "type은 필수입니다.") - boolean type + Boolean type ) { } From 01ab7b23ba2ac60918b1640e1d4d24c7a694b452 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 29 Jul 2025 02:16:12 +0900 Subject: [PATCH 17/19] =?UTF-8?q?[docs]=20=ED=8C=94=EB=A1=9C=EC=9E=89=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20api=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/adapter/in/web/UserQueryController.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index bd7f961af..a45e6f6db 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -84,12 +84,14 @@ public BaseResponse showMyFollowing( return BaseResponse.ok(userGetFollowUsecase.getMyFollowing(userId, cursor, size)); } - /** - * 팔로잉 여부 조회 - */ + @Operation( + summary = "팔로잉 여부 조회", + description = "특정 사용자가 다른 사용자를 팔로우하고 있는지 확인합니다." + ) @GetMapping("/users/{targetUserId}/is-following") - public BaseResponse checkisFollowing(@UserId final Long userId, - @PathVariable final Long targetUserId) { + public BaseResponse checkisFollowing( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "팔로우 여부를 확인할 대상 사용자 ID") @PathVariable final Long targetUserId) { return BaseResponse.ok(UserIsFollowingResponse.of(userIsFollowingUsecase.isFollowing(userId, targetUserId))); } } From f8d2731650b0e65f2cedc5c9946714acbb57500e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 29 Jul 2025 02:19:53 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[docs]=20=EB=82=B4=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EB=B0=A9=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EB=AA=85=EC=84=B8=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/RoomQueryController.java | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 8f945d81d..c16e0c8dd 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -3,30 +3,17 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.swagger.annotation.ExceptionDescription; -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; -import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse; -import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse; -import konkuk.thip.room.adapter.in.web.response.RoomGetMemberListResponse; -import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; +import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; import konkuk.thip.room.adapter.in.web.response.*; import konkuk.thip.room.application.port.in.*; -import konkuk.thip.room.application.port.in.RoomGetHomeJoinedListUseCase; -import konkuk.thip.room.application.port.in.RoomGetMemberListUseCase; -import konkuk.thip.room.application.port.in.RoomSearchUseCase; -import jakarta.validation.Valid; -import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; import konkuk.thip.room.application.port.in.dto.RoomGetHomeJoinedListQuery; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.*; -import java.time.LocalDate; - import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; @Tag(name = "Room Query API", description = "방 조회 관련 API") @@ -123,10 +110,16 @@ public BaseResponse getPlayingRoomDetailView( } // 내 모임방 리스트 조회 + @Operation( + summary = "내 모임방 리스트 조회", + description = "사용자가 참여중인 모임방 목록을 조회합니다. 타입에 따라 모집중인 방과 진행중인 방을 구분할 수 있습니다." + ) @GetMapping("/rooms/my") public BaseResponse getMyRooms( - @UserId final Long userId, + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "조회할 방의 타입 (playingAndRecruiting: 진행중인 방과 모집중인 방, recruiting: 모집중인 방만, playing: 진행중인 방만, expired: 만료된 방만)", example = "playingAndRecruiting") @RequestParam(value = "type", required = false, defaultValue = "playingAndRecruiting") final String type, + @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor) { return BaseResponse.ok(roomShowMineUseCase.getMyRooms(userId, type, cursor)); } From 279ad57381eb8127fccde91f79f18f10566adf08 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 30 Jul 2025 14:16:02 +0900 Subject: [PATCH 19/19] =?UTF-8?q?[fix]=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/request/FeedCreateRequest.java | 2 +- .../konkuk/thip/user/adapter/in/web/UserQueryController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java index 960d7174b..80e3cc92b 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java @@ -10,7 +10,7 @@ @Schema(description = "피드 생성 요청 DTO") public record FeedCreateRequest( - @Schema(description = "생성할 피드의 책 ISBN", example = "9788936433862") + @Schema(description = "생성할 피드의 책 ISBN", example = "9780306406157") @NotBlank(message = "ISBN은 필수입니다.") String isbn, diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index a45e6f6db..490538119 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -89,7 +89,7 @@ public BaseResponse showMyFollowing( description = "특정 사용자가 다른 사용자를 팔로우하고 있는지 확인합니다." ) @GetMapping("/users/{targetUserId}/is-following") - public BaseResponse checkisFollowing( + public BaseResponse checkIsFollowing( @Parameter(hidden = true) @UserId final Long userId, @Parameter(description = "팔로우 여부를 확인할 대상 사용자 ID") @PathVariable final Long targetUserId) { return BaseResponse.ok(UserIsFollowingResponse.of(userIsFollowingUsecase.isFollowing(userId, targetUserId)));