diff --git a/build.gradle b/build.gradle index 20159cd..5a2d1d0 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + // Swagger + implementation 'org.springdoc:springdoc-openapi-ui:1.6.6' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'io.rest-assured:rest-assured' diff --git a/src/main/java/today/seasoning/seasoning/article/controller/ArticleController.java b/src/main/java/today/seasoning/seasoning/article/controller/ArticleController.java index 0f5d491..196323a 100644 --- a/src/main/java/today/seasoning/seasoning/article/controller/ArticleController.java +++ b/src/main/java/today/seasoning/seasoning/article/controller/ArticleController.java @@ -2,6 +2,15 @@ import java.util.List; import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -37,6 +46,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/article") +@Tag(name = "Article", description = "기록장 API Document") public class ArticleController { private final RegisterArticleService registerArticleService; @@ -50,6 +60,13 @@ public class ArticleController { private final FindFriendArticlesService findFriendArticlesService; @PostMapping + @Operation(summary = "기록장 등록", description = "기록장을 등록합니다. (multipart/form-data 방식)", responses = { + @ApiResponse(responseCode = "200", description = "기록장 등록 성공 (등록한 기록장 id 반환)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "403", description = "기록장 열려있는 기간이 아님", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "요청 메시지 오류", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) + @Parameter(name = "images", description = "기록장 이미지 파일 리스트", required = false, schema = @Schema(type = "array", implementation = MultipartFile.class) + ) public ResponseEntity registerArticle( @AuthenticationPrincipal UserPrincipal principal, @RequestPart(name = "images", required = false) List images, @@ -60,6 +77,13 @@ public ResponseEntity registerArticle( } @GetMapping("/{articleId}") + @Operation(summary = "기록장 조회", description = "기록장을 조회합니다.", responses = { + @ApiResponse(responseCode = "200",description = "기록장 조회 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ArticleResponse.class))), + @ApiResponse(responseCode = "403", description = "기록장 조회 실패 (조회 권한 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "기록장 조회 실패 (기록장 id로 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) + @Parameter(name = "articleId",description = "기록장 id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string") + ) public ResponseEntity findArticle( @AuthenticationPrincipal UserPrincipal principal, @PathVariable String articleId @@ -69,6 +93,15 @@ public ResponseEntity findArticle( } @PutMapping("/{articleId}") + @Operation(summary = "기록장 수정", description = "기록장을 수정합니다. (multipart/form-data 방식)", responses = { + @ApiResponse(responseCode = "200", description = "기록장 수정 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "403", description = "기록장 수정 실패 (열려있는 기간이 아니거나 수정 권한 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "기록장 수정 실패 (기록장 id로 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "기록장 수정 실패 (이미지 개수 초과)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }, parameters = { + @Parameter(name = "images",description = "기록장 이미지 파일 리스트",required = false,schema = @Schema(type = "array", implementation = MultipartFile.class)), + @Parameter(name = "articleId",description = "기록장 id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string")) + }) public ResponseEntity updateArticle(@AuthenticationPrincipal UserPrincipal userPrincipal, @RequestPart(name = "images", required = false) List images, @RequestPart("request") @Valid UpdateArticleRequest request, @@ -79,6 +112,13 @@ public ResponseEntity updateArticle(@AuthenticationPrincipal UserPrincipal } @DeleteMapping("/{articleId}") + @Operation(summary = "기록장 삭제", description = "기록장을 삭제합니다.", responses = { + @ApiResponse(responseCode = "200", description = "기록장 삭제 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "403", description = "기록장 조회 실패 (조회 권한 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "기록장 조회 실패 (기록장 id로 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) + @Parameter(name = "articleId", description = "기록장 id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string") + ) public ResponseEntity deleteArticle( @AuthenticationPrincipal UserPrincipal principal, @PathVariable String articleId @@ -88,6 +128,11 @@ public ResponseEntity deleteArticle( } @GetMapping("/list/year/{year}") + @Operation(summary = "기록장 연도별 조회", description = "기록장을 연도별로 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "기록장 조회 성공", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = FindMyArticlesByYearResult.class)))) + }) + @Parameter(name = "year", description = "조회할 연도", required = true, in = ParameterIn.PATH, schema = @Schema(type = "Integer") + ) public ResponseEntity> findMyArticlesByYear( @AuthenticationPrincipal UserPrincipal principal, @PathVariable Integer year @@ -97,6 +142,11 @@ public ResponseEntity> findMyArticlesByYear( } @GetMapping("/list/term/{term}") + @Operation(summary = "기록장 절기별 조회", description = "기록장을 절기별로 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "기록장 절기별 조회 성공", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = FindMyArticlesByTermResult.class)))) + }) + @Parameter(name = "term", description = "조회할 절기", required = true, in = ParameterIn.PATH, schema = @Schema(type = "Integer") + ) public ResponseEntity> findMyArticlesByTerm( @AuthenticationPrincipal UserPrincipal principal, @PathVariable Integer term @@ -106,6 +156,12 @@ public ResponseEntity> findMyArticlesByTerm( } @PostMapping("{articleId}/like") + @Operation(summary = "기록장 좋아요", description = "기록장에 좋아요를 누릅니다.", responses = { + @ApiResponse(responseCode = "200", description = "기록장 좋아요 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "409", description = "기록장 좋아요 실패 (이미 좋아요를 누름)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) + @Parameter(name = "articleId", description = "기록장 id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string") + ) public ResponseEntity likeArticle( @AuthenticationPrincipal UserPrincipal principal, @PathVariable String articleId @@ -115,6 +171,12 @@ public ResponseEntity likeArticle( } @DeleteMapping("{articleId}/like") + @Operation(summary = "기록장 좋아요 취소", description = "기록장에 좋아요를 취소합니다.", responses = { + @ApiResponse(responseCode = "200",description = "기록장 좋아요 취소 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "기록장 좋아요 취소 실패 (누른 좋아요가 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) + @Parameter(name = "articleId", description = "기록장 id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string") + ) public ResponseEntity cancelLikeArticle( @AuthenticationPrincipal UserPrincipal principal, @PathVariable String articleId @@ -124,6 +186,9 @@ public ResponseEntity cancelLikeArticle( } @GetMapping("/collage") + @Operation(summary = "콜라주 조회", description = "콜라주를 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "콜라주 조회 성공", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = FindCollageResult.class)))) + }) public ResponseEntity> findCollage( @AuthenticationPrincipal UserPrincipal principal, @RequestParam("year") Integer year @@ -133,6 +198,12 @@ public ResponseEntity> findCollage( } @GetMapping("/friends") + @Operation(summary = "친구 기록장 목록 조회", description = "친구 기록장 목록을 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "친구 기록장 목록 조회 성공", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = FindFriendArticleResponse.class)))) + }, parameters = { + @Parameter(name = "lastId", description = "첫 조회 시, 생략, 이후 마지막으로 조회한 기록장의 id", in = ParameterIn.QUERY, schema = @Schema(type = "string")), + @Parameter(name = "size", description = "페이지 크기", in = ParameterIn.QUERY, schema = @Schema(type = "Integer")) + }) public ResponseEntity> findMyFriendsArticles( @AuthenticationPrincipal UserPrincipal principal, @RequestParam(name = "lastId", defaultValue = "AzL8n0Y58m7") String lastArticleId, diff --git a/src/main/java/today/seasoning/seasoning/article/dto/ArticleResponse.java b/src/main/java/today/seasoning/seasoning/article/dto/ArticleResponse.java index 627d0a0..e50acd6 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/ArticleResponse.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/ArticleResponse.java @@ -1,6 +1,8 @@ package today.seasoning.seasoning.article.dto; import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -11,15 +13,24 @@ @Getter @Builder @RequiredArgsConstructor +@Schema(title = "기록장 조회 응답") public class ArticleResponse { + @Schema(description = "기록장 공개 여부", example = "True") private final boolean published; + @Schema(description = "연도", example = "2024") private final int year; + @Schema(description = "절기 순번", example = "2") private final int term; + @Schema(description = "본문") private final String contents; + @Schema(description = "본문 이미지 리스트") private final List images; + @Schema(description = "사용자 프로필") private final UserProfileResponse profile; + @Schema(description = "좋아요 수", example = "3") private final int likesCount; + @Schema(description = "좋아요 여부", example = "True") private final boolean userLikes; public static ArticleResponse build(Long userId, Article article) { diff --git a/src/main/java/today/seasoning/seasoning/article/dto/FindCollageResult.java b/src/main/java/today/seasoning/seasoning/article/dto/FindCollageResult.java index bf2f9a4..308c0d3 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/FindCollageResult.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/FindCollageResult.java @@ -1,12 +1,17 @@ package today.seasoning.seasoning.article.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(title = "콜라주 조회 응답") public class FindCollageResult { + @Schema(description = "절기") private final int term; + @Schema(description = "기록장 id") private final String articleId; + @Schema(description = "이미지") private final String image; public FindCollageResult(int term, String articleId, String image) { diff --git a/src/main/java/today/seasoning/seasoning/article/dto/FindFriendArticleResponse.java b/src/main/java/today/seasoning/seasoning/article/dto/FindFriendArticleResponse.java index 7b442c1..2009b98 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/FindFriendArticleResponse.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/FindFriendArticleResponse.java @@ -1,5 +1,6 @@ package today.seasoning.seasoning.article.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.RequiredArgsConstructor; import today.seasoning.seasoning.article.domain.Article; @@ -7,6 +8,7 @@ @Getter @RequiredArgsConstructor +@Schema(title = "친구 기록장 목록 조회 응답") public class FindFriendArticleResponse { private final UserProfileResponse profile; diff --git a/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByTermResult.java b/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByTermResult.java index 2d307a0..4983338 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByTermResult.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByTermResult.java @@ -1,13 +1,19 @@ package today.seasoning.seasoning.article.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(title = "기록장 절기별 조회 응답") public class FindMyArticlesByTermResult { + @Schema(description = "기록장 id") private final String id; + @Schema(description = "기록장 연도") private final int year; + @Schema(description = "기록장 미리보기") private final String preview; + @Schema(description = "기록장 이미지") private final String image; public FindMyArticlesByTermResult(String id, int year, String preview, String image) { diff --git a/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByYearResult.java b/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByYearResult.java index d7f3fd0..15e0ffa 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByYearResult.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/FindMyArticlesByYearResult.java @@ -1,11 +1,15 @@ package today.seasoning.seasoning.article.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(title = "기록장 연도별 조회 응답") public class FindMyArticlesByYearResult { + @Schema(description = "기록장 id") private final String id; + @Schema(description = "기록장 절기") private final int term; public FindMyArticlesByYearResult(String id, int term) { diff --git a/src/main/java/today/seasoning/seasoning/article/dto/RegisterArticleRequest.java b/src/main/java/today/seasoning/seasoning/article/dto/RegisterArticleRequest.java index 06cc9a3..b56e5fb 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/RegisterArticleRequest.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/RegisterArticleRequest.java @@ -2,6 +2,8 @@ import java.util.List; import javax.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -12,9 +14,11 @@ @Getter @Setter @NoArgsConstructor +@Schema(title = "기록장 등록 요청") public class RegisterArticleRequest { @NotNull + @Schema(description = "기록장 공개 여부", required = true, example = "True") private Boolean published; @NotNull diff --git a/src/main/java/today/seasoning/seasoning/article/dto/UpdateArticleRequest.java b/src/main/java/today/seasoning/seasoning/article/dto/UpdateArticleRequest.java index 3bd4432..b0d180b 100644 --- a/src/main/java/today/seasoning/seasoning/article/dto/UpdateArticleRequest.java +++ b/src/main/java/today/seasoning/seasoning/article/dto/UpdateArticleRequest.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import javax.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -13,16 +15,20 @@ @Getter @Setter @NoArgsConstructor +@Schema(title = "기록장 수정 요청") public class UpdateArticleRequest { @NotNull @JsonProperty("image_modified") + @Schema(description = "이미지 수정 여부", required = true) private Boolean imageModified; @NotNull + @Schema(description = "기록장 공개 여부", required = true, example = "True") private Boolean published; @NotNull + @Schema(description = "기록장 본문 내용", required = true) private String contents; public UpdateArticleCommand buildCommand(UserPrincipal principal, String articleId, List images) { diff --git a/src/main/java/today/seasoning/seasoning/common/UserPrincipal.java b/src/main/java/today/seasoning/seasoning/common/UserPrincipal.java index 86a4b7e..f580896 100644 --- a/src/main/java/today/seasoning/seasoning/common/UserPrincipal.java +++ b/src/main/java/today/seasoning/seasoning/common/UserPrincipal.java @@ -1,12 +1,15 @@ package today.seasoning.seasoning.common; import java.io.Serializable; + +import io.swagger.v3.oas.annotations.Hidden; import lombok.Getter; import today.seasoning.seasoning.common.enums.LoginType; import today.seasoning.seasoning.user.domain.Role; import today.seasoning.seasoning.user.domain.User; @Getter +@Hidden public class UserPrincipal implements Serializable { private static final long serialVersionUID = 1L; diff --git a/src/main/java/today/seasoning/seasoning/common/config/JwtFilter.java b/src/main/java/today/seasoning/seasoning/common/config/JwtFilter.java index 29706ce..6f7b734 100644 --- a/src/main/java/today/seasoning/seasoning/common/config/JwtFilter.java +++ b/src/main/java/today/seasoning/seasoning/common/config/JwtFilter.java @@ -88,7 +88,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) { "/oauth/login", "/favicon.ico", "/refresh", - "/monitoring"); + "/monitoring", + "/swagger-ui", + "/v3/api-docs/**", + "/bus/v3/api-docs/**", + "/api-docs/**"); String path = request.getRequestURI(); diff --git a/src/main/java/today/seasoning/seasoning/common/config/SecurityConfig.java b/src/main/java/today/seasoning/seasoning/common/config/SecurityConfig.java index 66f308c..f7aa5d2 100644 --- a/src/main/java/today/seasoning/seasoning/common/config/SecurityConfig.java +++ b/src/main/java/today/seasoning/seasoning/common/config/SecurityConfig.java @@ -30,12 +30,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws httpSecurity.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); httpSecurity.authorizeRequests() - .antMatchers("/oauth/login/**", "/refresh", "/favicon.ico", "/monitoring/**").permitAll() + .antMatchers("/oauth/login/**", "/refresh", "/favicon.ico", "/monitoring/**" + , "/swagger-ui/**", "/v3/api-docs/**", "/bus/v3/api-docs/**", "/api-docs/**").permitAll() .antMatchers(HttpMethod.GET, "/notice/**").permitAll() + .antMatchers("/v3/api-docs/**").permitAll() .antMatchers("/notice/**").hasAnyRole(Role.MANAGER.name(), Role.ADMIN.name()) .antMatchers("/admin/**").hasRole(Role.ADMIN.name()) .anyRequest().authenticated(); - return httpSecurity.build(); } } diff --git a/src/main/java/today/seasoning/seasoning/common/config/SwaggerConfig.java b/src/main/java/today/seasoning/seasoning/common/config/SwaggerConfig.java new file mode 100644 index 0000000..ba36b4b --- /dev/null +++ b/src/main/java/today/seasoning/seasoning/common/config/SwaggerConfig.java @@ -0,0 +1,60 @@ +package today.seasoning.seasoning.common.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public GroupedOpenApi friendApi() { + return GroupedOpenApi.builder() + .group("친구 관련 API") + .pathsToMatch("/friend/**") + .build(); + } + + @Bean + public GroupedOpenApi articleApi() { + return GroupedOpenApi.builder() + .group("게시물 관련 API") + .pathsToMatch("/article/**") + .build(); + } + + @Bean + public GroupedOpenApi solartermApi() { + return GroupedOpenApi.builder() + .group("절기 관련 API") + .pathsToMatch("/solarTerm/**") + .build(); + } + + @Bean + public GroupedOpenApi notificationApi() { + return GroupedOpenApi.builder() + .group("알림 관련 API") + .pathsToMatch("/notification/**") + .build(); + } + + @Bean + public GroupedOpenApi userApi() { + return GroupedOpenApi.builder() + .group("사용자 관련 API") + .pathsToMatch("/user/**") + .build(); + } + + @Bean + public OpenAPI springOpenAPI() { + return new OpenAPI() + .info(new Info().title("Seasoning API") + .description("시즈닝 API 명세서입니다.") + .version("v0.0.1")); + + } +} \ No newline at end of file diff --git a/src/main/java/today/seasoning/seasoning/friendship/controller/FriendshipController.java b/src/main/java/today/seasoning/seasoning/friendship/controller/FriendshipController.java index 8ded9c4..5c1bfff 100644 --- a/src/main/java/today/seasoning/seasoning/friendship/controller/FriendshipController.java +++ b/src/main/java/today/seasoning/seasoning/friendship/controller/FriendshipController.java @@ -2,6 +2,14 @@ import java.util.List; import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -27,6 +35,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/friend") +@Tag(name = "Friend", description = "친구 API Document") public class FriendshipController { private final UnfriendService unfriendService; @@ -38,6 +47,11 @@ public class FriendshipController { private final DeclineFriendRequestService declineFriendRequestService; @PostMapping("/add") + @Operation(summary = "친구 신청", description = "다른 사용자에게 친구 요청을 보냅니다.", method = "POST", responses = { + @ApiResponse(responseCode = "200", description = "친구 신청 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "409", description = "친구 신청 실패 (이미 친구이거나, 친구 신청을 받았거나, 친구 신청을 보낸 상태)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "친구 신청 실패 (사용자를 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity requestFriendship( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UserIdDto userIdDto @@ -51,6 +65,9 @@ public ResponseEntity requestFriendship( } @GetMapping("/list") + @Operation(summary = "친구 목록 조회", description = "특정 사용자의 친구 목록을 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "성공적으로 특정 사용자의 친구 목록을 가져옴", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = FindUserFriendsResponse.class)))) + }) public ResponseEntity> findUserFriends(@AuthenticationPrincipal UserPrincipal principal) { Long userId = principal.getId(); List response = findAllFriendsService.doService(userId); @@ -58,6 +75,10 @@ public ResponseEntity> findUserFriends(@Authentica } @PostMapping("/add/accept") + @Operation(summary = "친구 신청 수락", description = "친구 신청을 수락합니다.", responses = { + @ApiResponse(responseCode = "200", description = "친구 신청 수락 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "친구 신청 수락 실패", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity acceptFriendship( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UserIdDto userIdDto @@ -67,6 +88,10 @@ public ResponseEntity acceptFriendship( } @DeleteMapping("/add/cancel") + @Operation(summary = "친구 신청 취소", description = "친구 신청을 취소합니다.", responses = { + @ApiResponse(responseCode = "200", description = "친구 신청 취소 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "친구 신청 취소 실패 (사용자를 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity cancelFriendship( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UserIdDto userIdDto @@ -80,6 +105,10 @@ public ResponseEntity cancelFriendship( } @DeleteMapping("/add/decline") + @Operation(summary = "친구 신청 거절", description = "친구 신청을 거절합니다.", responses = { + @ApiResponse(responseCode = "200", description = "친구 신청 거절 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "친구 신청 거절 실패 (사용자를 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity declineFriendship( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UserIdDto userIdDto @@ -93,6 +122,10 @@ public ResponseEntity declineFriendship( } @DeleteMapping("/unfriend") + @Operation(summary = "친구 삭제", description = "친구를 삭제합니다.", responses = { + @ApiResponse(responseCode = "200", description = "친구 삭제 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "친구 삭제 실패 (사용자를 찾을 수 없음)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity deleteFriendship( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UserIdDto userIdDto @@ -105,6 +138,12 @@ public ResponseEntity deleteFriendship( } @GetMapping("/search") + @Operation(summary = "친구 검색", description = "사용자를 검색합니다.", responses = { + @ApiResponse(responseCode = "200", description = "성공적으로 친구를 검색함", content = @Content(mediaType = "application/json", schema = @Schema(implementation = SearchUserResult.class))), + @ApiResponse(responseCode = "400", description = "사용자를 찾을 수 없음", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) + @Parameter(name = "keyword", description = "검색할 사용자의 아이디", required = true, example = "linguu", schema = @Schema(type = "string") + ) public ResponseEntity searchFriend( @AuthenticationPrincipal UserPrincipal principal, @RequestParam("keyword") String friendAccountId diff --git a/src/main/java/today/seasoning/seasoning/friendship/dto/FindUserFriendsResponse.java b/src/main/java/today/seasoning/seasoning/friendship/dto/FindUserFriendsResponse.java index 09ff214..2a3e173 100644 --- a/src/main/java/today/seasoning/seasoning/friendship/dto/FindUserFriendsResponse.java +++ b/src/main/java/today/seasoning/seasoning/friendship/dto/FindUserFriendsResponse.java @@ -1,5 +1,6 @@ package today.seasoning.seasoning.friendship.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.RequiredArgsConstructor; import today.seasoning.seasoning.common.util.TsidUtil; @@ -7,11 +8,16 @@ @Getter @RequiredArgsConstructor +@Schema(title = "친구 목록 조회 응답") public class FindUserFriendsResponse { + @Schema(description = "사용자 id") private final String id; + @Schema(description = "사용자 닉네임", example = "이아린") private final String nickname; + @Schema(description = "사용자 계정 id", example = "linggu") private final String accountId; + @Schema(description = "사용자 프로필 이미지 url", example = "https://www.naver.com/") private final String profileImageUrl; public static FindUserFriendsResponse build(User friend) { diff --git a/src/main/java/today/seasoning/seasoning/friendship/dto/SearchUserResult.java b/src/main/java/today/seasoning/seasoning/friendship/dto/SearchUserResult.java index 3209d09..9124592 100644 --- a/src/main/java/today/seasoning/seasoning/friendship/dto/SearchUserResult.java +++ b/src/main/java/today/seasoning/seasoning/friendship/dto/SearchUserResult.java @@ -1,5 +1,6 @@ package today.seasoning.seasoning.friendship.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.RequiredArgsConstructor; import today.seasoning.seasoning.common.enums.FriendshipStatus; @@ -8,12 +9,18 @@ @Getter @RequiredArgsConstructor +@Schema(title = "친구 검색 응답") public class SearchUserResult { + @Schema(description = "사용자 id", example = "이아린") private final String id; + @Schema(description = "사용자 닉네임", example = "이아린") private final String nickname; + @Schema(description = "사용자 프로필 이미지 url", example = "https://www.naver.com/") private final String image; + @Schema(description = "사용자 계정 id", example = "linguu") private final String accountId; + @Schema(description = "현재 친구 관계", example = "FRIEND") private FriendshipStatus friendshipStatus; public SearchUserResult(String id, String nickname, String image, String accountId, FriendshipStatus friendshipStatus) { diff --git a/src/main/java/today/seasoning/seasoning/friendship/dto/UserIdDto.java b/src/main/java/today/seasoning/seasoning/friendship/dto/UserIdDto.java index 8e4d29d..2c1eb7a 100644 --- a/src/main/java/today/seasoning/seasoning/friendship/dto/UserIdDto.java +++ b/src/main/java/today/seasoning/seasoning/friendship/dto/UserIdDto.java @@ -1,6 +1,8 @@ package today.seasoning.seasoning.friendship.dto; import javax.validation.constraints.NotBlank; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -9,6 +11,7 @@ @Getter @Setter @NoArgsConstructor +@Schema(title = "사용자 아이디") public class UserIdDto { @NotBlank diff --git a/src/main/java/today/seasoning/seasoning/notification/controller/NotificationController.java b/src/main/java/today/seasoning/seasoning/notification/controller/NotificationController.java index 90b0267..954b339 100644 --- a/src/main/java/today/seasoning/seasoning/notification/controller/NotificationController.java +++ b/src/main/java/today/seasoning/seasoning/notification/controller/NotificationController.java @@ -1,6 +1,15 @@ package today.seasoning.seasoning.notification.controller; import java.util.List; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -17,12 +26,20 @@ @RestController @RequiredArgsConstructor @RequestMapping("/notification") +@Tag(name = "Notification", description = "알림 API Document") public class NotificationController { private final FindNotificationsService findNotificationsService; private final CheckUnreadNotificationsExistService checkUnreadNotificationsExistService; @GetMapping + @Operation(summary = "알림 조회", description = "사용자의 알림을 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "알림 조회 성공", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = UserNotificationResponse.class)))), + @ApiResponse(responseCode = "404", description = "알림 조회 실패 (사용자 조회 실패)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }, parameters = { + @Parameter(name = "lastId", description = "마지막으로 조회한 알림의 아이디", example = "AzL8n0Y58m7", in = ParameterIn.QUERY, schema = @Schema(type = "string", defaultValue = "AzL8n0Y58m7")), + @Parameter(name = "size", description = "페이지 크기", example = "10", in = ParameterIn.QUERY, schema = @Schema(type = "integer", defaultValue = "10")) + }) public ResponseEntity> findNotifications( @AuthenticationPrincipal UserPrincipal principal, @RequestParam(name = "lastId", defaultValue = "AzL8n0Y58m7") String lastId, @@ -34,6 +51,10 @@ public ResponseEntity> findNotifications( } @GetMapping("/new") + @Operation(summary = "새로운 알림 조회", description = "사용자의 새로운 알림 유무를 조회합니다.", responses = { + @ApiResponse(responseCode = "200", description = "새로운 알림 유무 조회 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "boolean"))), + @ApiResponse(responseCode = "404", description = "새로운 알림 유무 조회 실패 (사용자 조회 실패)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity checkUnreadNotificationsExist(@AuthenticationPrincipal UserPrincipal principal) { boolean result = checkUnreadNotificationsExistService.doService(principal.getId()); return ResponseEntity.ok(result); diff --git a/src/main/java/today/seasoning/seasoning/notification/dto/UserNotificationResponse.java b/src/main/java/today/seasoning/seasoning/notification/dto/UserNotificationResponse.java index ffdbb0a..d154901 100644 --- a/src/main/java/today/seasoning/seasoning/notification/dto/UserNotificationResponse.java +++ b/src/main/java/today/seasoning/seasoning/notification/dto/UserNotificationResponse.java @@ -1,6 +1,8 @@ package today.seasoning.seasoning.notification.dto; import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -10,13 +12,19 @@ @Getter @Builder @RequiredArgsConstructor +@Schema(title = "알림 응답") public class UserNotificationResponse { + @Schema(description = "알림 id") private final String id; + @Schema(description = "알림 종류") private final String type; + @Schema(description = "알림 일자") private final LocalDateTime created; private final UserProfileResponse profile; + @Schema(description = "알림 내용") private final String message; + @Schema(description = "읽음 여부") private final boolean read; public static UserNotificationResponse build(UserNotificationProjectionInterface u) { diff --git a/src/main/java/today/seasoning/seasoning/solarterm/controller/SolarTermController.java b/src/main/java/today/seasoning/seasoning/solarterm/controller/SolarTermController.java index 32b3bee..c78844c 100644 --- a/src/main/java/today/seasoning/seasoning/solarterm/controller/SolarTermController.java +++ b/src/main/java/today/seasoning/seasoning/solarterm/controller/SolarTermController.java @@ -1,5 +1,10 @@ package today.seasoning.seasoning.solarterm.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -12,6 +17,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "SolarTerm", description = "절기 조회 API Document") public class SolarTermController { private final SolarTermService solarTermService; @@ -24,6 +30,8 @@ public ResponseEntity registerSolarTerms(@RequestParam("year") int year) { } @GetMapping("/solarTerm") + @Operation(summary = "절기 조회", description = "절기 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "절기 정보 조회 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = FindSolarTermInfoResponse.class))) public ResponseEntity findSolarTermInfo() { FindSolarTermInfoResponse solarTermInfoResponse = solarTermService.findSolarTermInfo(); return ResponseEntity.ok(solarTermInfoResponse); diff --git a/src/main/java/today/seasoning/seasoning/solarterm/dto/FindSolarTermInfoResponse.java b/src/main/java/today/seasoning/seasoning/solarterm/dto/FindSolarTermInfoResponse.java index 33d4a25..d3e1159 100644 --- a/src/main/java/today/seasoning/seasoning/solarterm/dto/FindSolarTermInfoResponse.java +++ b/src/main/java/today/seasoning/seasoning/solarterm/dto/FindSolarTermInfoResponse.java @@ -1,16 +1,22 @@ package today.seasoning.seasoning.solarterm.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import today.seasoning.seasoning.solarterm.domain.SolarTerm; @Getter @AllArgsConstructor +@Schema(title = "절기 조회 응답") public class FindSolarTermInfoResponse { + @Schema(description = "기록장 열림 여부") private final boolean recordable; + @Schema(description = "시간상 현재 절기") private final SolarTermDto currentTerm; + @Schema(description = "시간상 다음 절기") private final SolarTermDto nextTerm; + @Schema(description = "기록장 등록 가능한 절기") private final SolarTermDto recordTerm; public static FindSolarTermInfoResponse build( diff --git a/src/main/java/today/seasoning/seasoning/solarterm/dto/SolarTermDto.java b/src/main/java/today/seasoning/seasoning/solarterm/dto/SolarTermDto.java index 8d125c0..050f025 100644 --- a/src/main/java/today/seasoning/seasoning/solarterm/dto/SolarTermDto.java +++ b/src/main/java/today/seasoning/seasoning/solarterm/dto/SolarTermDto.java @@ -1,6 +1,8 @@ package today.seasoning.seasoning.solarterm.dto; import java.time.LocalDate; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.RequiredArgsConstructor; import today.seasoning.seasoning.solarterm.domain.SolarTerm; @@ -9,7 +11,9 @@ @RequiredArgsConstructor public class SolarTermDto { + @Schema(description = "절기 순번", example = "1") private final int sequence; + @Schema(description = "기록 가능한 마지막 날짜") private final LocalDate date; public static SolarTermDto build(SolarTerm solarTerm) { diff --git a/src/main/java/today/seasoning/seasoning/user/controller/UserController.java b/src/main/java/today/seasoning/seasoning/user/controller/UserController.java index c0fe923..2c8b0f4 100644 --- a/src/main/java/today/seasoning/seasoning/user/controller/UserController.java +++ b/src/main/java/today/seasoning/seasoning/user/controller/UserController.java @@ -1,6 +1,13 @@ package today.seasoning.seasoning.user.controller; import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -30,6 +37,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/user") +@Tag(name = "User", description = "사용자 API Document") public class UserController { private final UpdateUserProfileService updateUserProfile; @@ -41,6 +49,8 @@ public class UserController { // 프로필 조회 @GetMapping("/profile") + @Operation(summary = "프로필 조회", description = "프로필을 조회합니다.") + @ApiResponse(responseCode = "200", description = "프로필 조회 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserProfileResponse.class))) public ResponseEntity findUserProfile(@AuthenticationPrincipal UserPrincipal userPrincipal) { UserProfileResponse userProfile = findUserProfileService.findUserProfile(userPrincipal.getId()); return ResponseEntity.ok().body(userProfile); @@ -48,6 +58,8 @@ public ResponseEntity findUserProfile(@AuthenticationPrinci // 프로필 수정 @PutMapping("/profile") + @Operation(summary = "프로필 수정", description = "프로필을 수정합니다. (multipart/form-data 방식)") + @ApiResponse(responseCode = "200", description = "프로필 수정 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) public ResponseEntity updateUserProfile( @AuthenticationPrincipal UserPrincipal principal, @RequestPart(name = "image", required = false) MultipartFile profileImage, @@ -58,6 +70,10 @@ public ResponseEntity updateUserProfile( } @GetMapping("/check-account-id") + @Operation(summary = "아이디 검증", description = "아이디를 검증합니다.", responses = { + @ApiResponse(responseCode = "200", description = "아이디 검증 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "409", description = "아이디 검증 실패 (이미 등록된 아이디)", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + }) public ResponseEntity checkAccountId(@RequestParam("id") String accountId) { if (verifyAccountIdService.verify(new AccountId(accountId))) { return ResponseEntity.ok().build(); @@ -66,18 +82,26 @@ public ResponseEntity checkAccountId(@RequestParam("id") String accountId) } @DeleteMapping + @Operation(summary = "계정 탈퇴", description = "계정을 탈퇴합니다.") + @ApiResponse(responseCode = "200", description = "계정 탈퇴 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) public ResponseEntity unregister(@AuthenticationPrincipal UserPrincipal principal) { deleteUserService.doService(principal.getId()); return ResponseEntity.ok().build(); } @GetMapping("/searchable") + @Operation(summary = "검색 허용 상태 조회", description = "검색 허용 상태를 조회합니다.") + @ApiResponse(responseCode = "200", description = "검색 허용 상태 조회 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "boolean"))) public ResponseEntity findSearchableStatus(@AuthenticationPrincipal UserPrincipal principal) { boolean searchable = findUserSearchableStatusService.doService(principal.getId()); return ResponseEntity.ok(searchable); } @RequestMapping(value = "", method = RequestMethod.PUT, params = "searchable") + @Operation(summary = "검색 허용 상태 변경", description = "검색 허용 상태를 변경합니다.") + @ApiResponse(responseCode = "200", description = "검색 허용 상태 변경 성공", content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) + @Parameter(name = "searchable", description = "검색 허용 여부", required = true, example = "true", schema = @Schema(type = "boolean") + ) public ResponseEntity updateSearchableStatus( @AuthenticationPrincipal UserPrincipal principal, @RequestParam("searchable") boolean searchable diff --git a/src/main/java/today/seasoning/seasoning/user/dto/UpdateUserProfileRequest.java b/src/main/java/today/seasoning/seasoning/user/dto/UpdateUserProfileRequest.java index b89e48a..bf51289 100644 --- a/src/main/java/today/seasoning/seasoning/user/dto/UpdateUserProfileRequest.java +++ b/src/main/java/today/seasoning/seasoning/user/dto/UpdateUserProfileRequest.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -12,6 +14,7 @@ @Getter @Setter @NoArgsConstructor +@Schema(title = "사용자 프로필 수정 요청") public class UpdateUserProfileRequest { @NotNull diff --git a/src/main/java/today/seasoning/seasoning/user/dto/UserProfileResponse.java b/src/main/java/today/seasoning/seasoning/user/dto/UserProfileResponse.java index 87a020d..ab8832d 100644 --- a/src/main/java/today/seasoning/seasoning/user/dto/UserProfileResponse.java +++ b/src/main/java/today/seasoning/seasoning/user/dto/UserProfileResponse.java @@ -1,5 +1,6 @@ package today.seasoning.seasoning.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -9,6 +10,7 @@ @Getter @Setter @NoArgsConstructor +@Schema(title = "사용자 프로필 조회 응답") public class UserProfileResponse { private String id;