Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,6 +46,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/article")
@Tag(name = "Article", description = "기록장 API Document")
public class ArticleController {

private final RegisterArticleService registerArticleService;
Expand All @@ -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<String> registerArticle(
@AuthenticationPrincipal UserPrincipal principal,
@RequestPart(name = "images", required = false) List<MultipartFile> images,
Expand All @@ -60,6 +77,13 @@ public ResponseEntity<String> 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<ArticleResponse> findArticle(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String articleId
Expand All @@ -69,6 +93,15 @@ public ResponseEntity<ArticleResponse> 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<Void> updateArticle(@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestPart(name = "images", required = false) List<MultipartFile> images,
@RequestPart("request") @Valid UpdateArticleRequest request,
Expand All @@ -79,6 +112,13 @@ public ResponseEntity<Void> 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<Void> deleteArticle(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String articleId
Expand All @@ -88,6 +128,11 @@ public ResponseEntity<Void> 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<List<FindMyArticlesByYearResult>> findMyArticlesByYear(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Integer year
Expand All @@ -97,6 +142,11 @@ public ResponseEntity<List<FindMyArticlesByYearResult>> 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<List<FindMyArticlesByTermResult>> findMyArticlesByTerm(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Integer term
Expand All @@ -106,6 +156,12 @@ public ResponseEntity<List<FindMyArticlesByTermResult>> 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<Void> likeArticle(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String articleId
Expand All @@ -115,6 +171,12 @@ public ResponseEntity<Void> 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<Void> cancelLikeArticle(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String articleId
Expand All @@ -124,6 +186,9 @@ public ResponseEntity<Void> 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<List<FindCollageResult>> findCollage(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam("year") Integer year
Expand All @@ -133,6 +198,12 @@ public ResponseEntity<List<FindCollageResult>> 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<List<FindFriendArticleResponse>> findMyFriendsArticles(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam(name = "lastId", defaultValue = "AzL8n0Y58m7") String lastArticleId,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<ArticleImageResponse> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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;
import today.seasoning.seasoning.user.dto.UserProfileResponse;

@Getter
@RequiredArgsConstructor
@Schema(title = "친구 기록장 목록 조회 응답")
public class FindFriendArticleResponse {

private final UserProfileResponse profile;
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,9 +14,11 @@
@Getter
@Setter
@NoArgsConstructor
@Schema(title = "기록장 등록 요청")
public class RegisterArticleRequest {

@NotNull
@Schema(description = "기록장 공개 여부", required = true, example = "True")
private Boolean published;

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MultipartFile> images) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading