-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 골격 작성 * feat: 엔티티 작성 * feat: repository 작성 * feat: service 작성 * feat: response 작성 * feat: DB 조회 로직 작성 * feat: Jsoup 의존성 추가 * feat: 응답 내용 추가 1. article.is_notice 2. article.contentSummary 3. board.is_notice 4. board.children (must null) * feat: 예외처리 작성 * refactor: 페이징 관련 정보 클래스 분리 * fix: soft delete된 게시판 or 게시글은 조회 안되도록 수정 * fix: 1페이지 조회 안되는 버그 수정 * fix: 누락된 응답 추가 (summary) * fix: 최신글부터 조회하도록 수정 * test: 테스트 코드 작성 * move: ErrorResponseWrapper 위치 변경 * refactor: 코딩 컨벤션 준수 * refactor: 문자열 파싱 메서드 리팩토링 * test: 예외상황 테스트 추가
- Loading branch information
1 parent
d491515
commit bbbe5be
Showing
13 changed files
with
865 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
src/main/java/in/koreatech/koin/domain/community/controller/CommunityController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package in.koreatech.koin.domain.community.controller; | ||
|
||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import in.koreatech.koin.domain.community.dto.ArticlesResponse; | ||
import in.koreatech.koin.domain.community.service.CommunityService; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
public class CommunityController { | ||
|
||
private final CommunityService communityService; | ||
|
||
@GetMapping("/articles") | ||
public ResponseEntity<ArticlesResponse> getArticles( | ||
@RequestParam Long boardId, | ||
@RequestParam(required = false) Long page, | ||
@RequestParam(required = false) Long limit | ||
) { | ||
ArticlesResponse foundArticles = communityService.getArticles(boardId, page, limit); | ||
return ResponseEntity.ok().body(foundArticles); | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
src/main/java/in/koreatech/koin/domain/community/dto/ArticlesResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package in.koreatech.koin.domain.community.dto; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
|
||
import com.fasterxml.jackson.annotation.JsonFormat; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
|
||
import in.koreatech.koin.domain.community.model.Article; | ||
import in.koreatech.koin.domain.community.model.Board; | ||
|
||
public record ArticlesResponse( | ||
List<InnerArticleResponse> articles, | ||
InnerBoardResponse board, | ||
Long totalPage | ||
) { | ||
|
||
public static ArticlesResponse of(List<Article> articles, Board board, Long totalPage) { | ||
return new ArticlesResponse( | ||
articles.stream() | ||
.map(InnerArticleResponse::from) | ||
.toList(), | ||
InnerBoardResponse.from(board), | ||
totalPage | ||
); | ||
} | ||
|
||
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
private record InnerArticleResponse( | ||
Long id, | ||
Long boardId, | ||
String title, | ||
String content, | ||
Long userId, | ||
String nickname, | ||
Long hit, String ip, | ||
Boolean isSolved, | ||
Boolean isDeleted, | ||
Byte commentCount, | ||
String meta, | ||
Boolean isNotice, | ||
Long noticeArticleId, | ||
String summary, | ||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, | ||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updatedAt, | ||
@JsonProperty("contentSummary") String contentSummary | ||
) { | ||
|
||
public static InnerArticleResponse from(Article article) { | ||
return new InnerArticleResponse( | ||
article.getId(), | ||
article.getBoardId(), | ||
article.getTitle(), | ||
article.getContent(), | ||
article.getUserId(), | ||
article.getNickname(), | ||
article.getHit(), | ||
article.getIp(), | ||
article.getIsSolved(), | ||
article.getIsDeleted(), | ||
article.getCommentCount(), | ||
article.getMeta(), | ||
article.getIsNotice(), | ||
article.getNoticeArticleId(), | ||
article.getSummary(), | ||
article.getCreatedAt(), | ||
article.getUpdatedAt(), | ||
article.getContentSummary() | ||
); | ||
} | ||
} | ||
|
||
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
public record InnerBoardResponse( | ||
Long id, | ||
String tag, | ||
String name, | ||
Boolean isAnonymous, | ||
Long articleCount, | ||
Boolean isDeleted, | ||
Boolean isNotice, | ||
Long parentId, | ||
Long seq, | ||
List<InnerBoardResponse> children, | ||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, | ||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updatedAt | ||
) { | ||
|
||
public static InnerBoardResponse from(Board board) { | ||
return new InnerBoardResponse( | ||
board.getId(), | ||
board.getTag(), | ||
board.getName(), | ||
board.getIsAnonymous(), | ||
board.getArticleCount(), | ||
board.getIsDeleted(), | ||
board.getIsNotice(), | ||
board.getParentId(), | ||
board.getSeq(), | ||
board.getChildren().isEmpty() | ||
? null : board.getChildren().stream().map(InnerBoardResponse::from).toList(), | ||
board.getCreatedAt(), | ||
board.getUpdatedAt() | ||
); | ||
} | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
src/main/java/in/koreatech/koin/domain/community/exception/ArticleNotFoundException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package in.koreatech.koin.domain.community.exception; | ||
|
||
public class ArticleNotFoundException extends RuntimeException { | ||
private static final String DEFAULT_MESSAGE = "게시글이 존재하지 않습니다."; | ||
|
||
public ArticleNotFoundException(String message) { | ||
super(message); | ||
} | ||
|
||
public static ArticleNotFoundException withDetail(String detail) { | ||
String message = String.format("%s %s", DEFAULT_MESSAGE, detail); | ||
return new ArticleNotFoundException(message); | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
src/main/java/in/koreatech/koin/domain/community/model/Article.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package in.koreatech.koin.domain.community.model; | ||
|
||
import org.hibernate.annotations.Where; | ||
import org.jsoup.Jsoup; | ||
|
||
import in.koreatech.koin.global.common.BaseEntity; | ||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Lob; | ||
import jakarta.persistence.Table; | ||
import jakarta.persistence.Transient; | ||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
@Entity | ||
@Table(name = "articles") | ||
@Where(clause = "is_deleted=0") | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class Article extends BaseEntity { | ||
|
||
private static final int SUMMARY_MIN_LENGTH = 0; | ||
private static final int SUMMARY_MAX_LENGTH = 100; | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Column(name = "id", nullable = false) | ||
private Long id; | ||
|
||
@NotNull | ||
@Column(name = "board_id", nullable = false) | ||
private Long boardId; | ||
|
||
@Size(max = 255) | ||
@NotNull | ||
@Column(name = "title", nullable = false) | ||
private String title; | ||
|
||
@NotNull | ||
@Lob | ||
@Column(name = "content", nullable = false) | ||
private String content; | ||
|
||
@NotNull | ||
@Column(name = "user_id", nullable = false) | ||
private Long userId; | ||
|
||
@Size(max = 50) | ||
@NotNull | ||
@Column(name = "nickname", nullable = false, length = 50) | ||
private String nickname; | ||
|
||
@NotNull | ||
@Column(name = "hit", nullable = false) | ||
private Long hit; | ||
|
||
@Size(max = 45) | ||
@NotNull | ||
@Column(name = "ip", nullable = false, length = 45) | ||
private String ip; | ||
|
||
@NotNull | ||
@Column(name = "is_solved", nullable = false) | ||
private Boolean isSolved = false; | ||
|
||
@NotNull | ||
@Column(name = "is_deleted", nullable = false) | ||
private Boolean isDeleted = false; | ||
|
||
@NotNull | ||
@Column(name = "comment_count", nullable = false) | ||
private Byte commentCount; | ||
|
||
@Lob | ||
@Column(name = "meta") | ||
private String meta; | ||
|
||
@NotNull | ||
@Column(name = "is_notice", nullable = false) | ||
private Boolean isNotice = false; | ||
|
||
@Column(name = "notice_article_id") | ||
private Long noticeArticleId; | ||
|
||
@Transient | ||
private String summary; | ||
|
||
public String getContentSummary() { | ||
if (content == null) { | ||
return ""; | ||
} | ||
String contentSummary = Jsoup.parse(content).text(); | ||
contentSummary = contentSummary.replace(" ", "").strip(); | ||
if (contentSummary.length() < SUMMARY_MAX_LENGTH) { | ||
return contentSummary; | ||
} | ||
return contentSummary.substring(SUMMARY_MIN_LENGTH, SUMMARY_MAX_LENGTH); | ||
} | ||
|
||
@Builder | ||
private Article(Long boardId, String title, String content, Long userId, String nickname, Long hit, | ||
String ip, Boolean isSolved, Boolean isDeleted, Byte commentCount, String meta, Boolean isNotice, | ||
Long noticeArticleId) { | ||
this.boardId = boardId; | ||
this.title = title; | ||
this.content = content; | ||
this.userId = userId; | ||
this.nickname = nickname; | ||
this.hit = hit; | ||
this.ip = ip; | ||
this.isSolved = isSolved; | ||
this.isDeleted = isDeleted; | ||
this.commentCount = commentCount; | ||
this.meta = meta; | ||
this.isNotice = isNotice; | ||
this.noticeArticleId = noticeArticleId; | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
src/main/java/in/koreatech/koin/domain/community/model/Board.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package in.koreatech.koin.domain.community.model; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import org.hibernate.annotations.Where; | ||
|
||
import in.koreatech.koin.global.common.BaseEntity; | ||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Table; | ||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
@Entity | ||
@Table(name = "boards") | ||
@Where(clause = "is_deleted=0") | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class Board extends BaseEntity { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Column(name = "id", nullable = false) | ||
private Long id; | ||
|
||
@Size(max = 10) | ||
@NotNull | ||
@Column(name = "tag", nullable = false, length = 10) | ||
private String tag; | ||
|
||
@Size(max = 50) | ||
@NotNull | ||
@Column(name = "name", nullable = false, length = 50) | ||
private String name; | ||
|
||
@NotNull | ||
@Column(name = "is_anonymous", nullable = false) | ||
private Boolean isAnonymous = false; | ||
|
||
@NotNull | ||
@Column(name = "article_count", nullable = false) | ||
private Long articleCount; | ||
|
||
@NotNull | ||
@Column(name = "is_deleted", nullable = false) | ||
private Boolean isDeleted = false; | ||
|
||
@NotNull | ||
@Column(name = "is_notice", nullable = false) | ||
private Boolean isNotice = false; | ||
|
||
@Column(name = "parent_id") | ||
private Long parentId; | ||
|
||
@NotNull | ||
@Column(name = "seq", nullable = false) | ||
private Long seq; | ||
|
||
public List<Board> getChildren() { | ||
return new ArrayList<>(); | ||
} | ||
|
||
@Builder | ||
private Board(String tag, String name, Boolean isAnonymous, Long articleCount, Boolean isDeleted, | ||
Boolean isNotice, Long parentId, Long seq) { | ||
this.tag = tag; | ||
this.name = name; | ||
this.isAnonymous = isAnonymous; | ||
this.articleCount = articleCount; | ||
this.isDeleted = isDeleted; | ||
this.isNotice = isNotice; | ||
this.parentId = parentId; | ||
this.seq = seq; | ||
} | ||
} |
Oops, something went wrong.