Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 게시판 기능을 구현한다 #14

Merged
merged 7 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:toc: left
:toclevels: 3

== 회원가입을 진행한다 (/api/signup)
== 회원가입을 진행한다 (POST /api/signup)

=== Request

Expand All @@ -17,7 +17,7 @@ include::{snippets}/auth-controller-web-mvc-test/do_signup/http-request.adoc[]
include::{snippets}/auth-controller-web-mvc-test/do_signup/response-fields.adoc[]
include::{snippets}/auth-controller-web-mvc-test/do_signup/http-response.adoc[]

== 로그인을 진행한다 (/api/login)
== 로그인을 진행한다 (POST /api/login)

=== Request

Expand Down
55 changes: 55 additions & 0 deletions src/docs/asciidoc/board.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
= Board API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== 게시글을 작성한다 (POST /api/boards)

=== Request

include::{snippets}/board-controller-web-mvc-test/save_board/request-headers.adoc[]
include::{snippets}/board-controller-web-mvc-test/save_board/request-parts.adoc[]

=== Response

include::{snippets}/board-controller-web-mvc-test/save_board/response-headers.adoc[]
include::{snippets}/board-controller-web-mvc-test/save_board/http-response.adoc[]

== 게시글을 단건 조회한다 (GET /api/boards/{id})

=== Request

include::{snippets}/board-controller-web-mvc-test/find_board_by_id/path-parameters.adoc[]
include::{snippets}/board-controller-web-mvc-test/find_board_by_id/http-request.adoc[]

=== Response

include::{snippets}/board-controller-web-mvc-test/find_board_by_id/response-fields.adoc[]
include::{snippets}/board-controller-web-mvc-test/find_board_by_id/http-response.adoc[]

== 게시글을 수정한다 (PATCH /api/boards/{id})

=== Request

include::{snippets}/board-controller-web-mvc-test/patch_board/request-headers.adoc[]
include::{snippets}/board-controller-web-mvc-test/patch_board/path-parameters.adoc[]
include::{snippets}/board-controller-web-mvc-test/patch_board/request-parts.adoc[]
include::{snippets}/board-controller-web-mvc-test/patch_board/http-request.adoc[]

=== Response

include::{snippets}/board-controller-web-mvc-test/patch_board/http-response.adoc[]

== 게시글을 삭제한다 (DELETE /api/boards/{id})

=== Request

include::{snippets}/board-controller-web-mvc-test/delete_board/request-headers.adoc[]
include::{snippets}/board-controller-web-mvc-test/delete_board/path-parameters.adoc[]
include::{snippets}/board-controller-web-mvc-test/delete_board/http-request.adoc[]

=== Response

include::{snippets}/board-controller-web-mvc-test/delete_board/http-response.adoc[]
1 change: 1 addition & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ endif::[]
:sectlinks:

include::auth.adoc[]
include::board.adoc[]
75 changes: 75 additions & 0 deletions src/main/java/com/market/board/application/BoardService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.market.board.application;

import com.market.board.application.dto.BoardCreateRequest;
import com.market.board.application.dto.BoardUpdateRequest;
import com.market.board.domain.board.Board;
import com.market.board.domain.board.BoardRepository;
import com.market.board.domain.board.BoardUpdateResult;
import com.market.board.domain.image.ImageConverter;
import com.market.board.domain.image.ImageUploader;
import com.market.board.exception.exceptions.BoardNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class BoardService {

private final BoardRepository boardRepository;
private final ImageConverter imageConverter;
private final ImageUploader imageUploader;

@Transactional
public Long saveBoard(final Long memberId, final BoardCreateRequest request) {
Board board = new Board(
request.title(),
request.content(),
memberId,
request.images(),
imageConverter
);

Board savedBoard = boardRepository.save(board);
imageUploader.upload(board.getImages(), request.images());

return savedBoard.getId();
}

@Transactional(readOnly = true)
public Board findBoardById(final Long boardId) {
return findBoard(boardId);
}

private Board findBoard(final Long boardId) {
return boardRepository.findById(boardId)
.orElseThrow(BoardNotFoundException::new);
}

@Transactional
public void patchBoardById(final Long boardId,
final Long memberId,
final BoardUpdateRequest request
) {
Board board = findBoardWithImages(boardId);
board.validateWriter(memberId);
BoardUpdateResult result = board.update(request.title(), request.content(), request.addedImages(), request.deletedImages(), imageConverter);

imageUploader.upload(result.getAddedImages(), request.addedImages());
imageUploader.delete(result.getDeletedImages());
}

private Board findBoardWithImages(final Long boardId) {
return boardRepository.findBoardWithImages(boardId)
.orElseThrow(BoardNotFoundException::new);
}

@Transactional
public void deleteBoardById(final Long boardId, final Long memberId) {
Board board = findBoard(boardId);
board.validateWriter(memberId);

boardRepository.deleteByBoardId(boardId);
imageUploader.delete(board.getImages());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.market.board.application.dto;

import jakarta.validation.constraints.NotBlank;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

public record BoardCreateRequest(
@NotBlank(message = "게시글 제목을 입력해주세요.")
String title,

@NotBlank(message = "게시글 내용을 작성해주세요.")
String content,

List<MultipartFile> images
) {

public BoardCreateRequest(final String title, final String content, final List<MultipartFile> images) {
this.title = title;
this.content = content;
this.images = handleAddedImages(images);
}

private List<MultipartFile> handleAddedImages(final List<MultipartFile> addedImages) {
if (addedImages == null) {
return new ArrayList<>();
}

return addedImages;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.market.board.application.dto;

import jakarta.validation.constraints.NotBlank;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

public record BoardUpdateRequest(

@NotBlank(message = "게시글 제목을 입력해주세요.")
String title,

@NotBlank(message = "게시글 내용을 작성해주세요.")
String content,

List<MultipartFile> addedImages,

List<Long> deletedImages
) {

public BoardUpdateRequest(final String title,
final String content,
final List<MultipartFile> addedImages,
final List<Long> deletedImages
) {
this.title = title;
this.content = content;
this.addedImages = handleAddedImages(addedImages);
this.deletedImages = handleDeletedImages(deletedImages);
}

private List<MultipartFile> handleAddedImages(final List<MultipartFile> addedImages) {
if (addedImages == null) {
return new ArrayList<>();
}

return addedImages;
}

private List<Long> handleDeletedImages(final List<Long> deletedImages) {
if (deletedImages == null) {
return new ArrayList<>();
}

return deletedImages;
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/market/board/domain/board/Board.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.market.board.domain.board;

import com.market.board.domain.image.Image;
import com.market.board.domain.image.ImageConverter;
import com.market.board.exception.exceptions.WriterNotEqualsException;
import com.market.global.domain.BaseEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

@Getter
@Builder
@EqualsAndHashCode(of = "id", callSuper = false)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class Board extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Embedded
private Post post;

@Column(nullable = false)
private Long writerId;

@JoinColumn(name = "board_id")
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Image> images = new ArrayList<>();

@Builder
public Board(final String title, final String content, final Long writerId, final List<MultipartFile> imageFiles, final ImageConverter imageConverter) {
this.post = Post.of(title, content);
this.writerId = writerId;
this.images.addAll(imageConverter.convertImageFilesToImages(imageFiles));
}

public BoardUpdateResult update(final String title, final String content, final List<MultipartFile> imageFiles, final List<Long> deletedImageIds, final ImageConverter imageConverter) {
post.update(title, content);

List<Image> addedImages = imageConverter.convertImageFilesToImages(imageFiles);
List<Image> deletedImages = imageConverter.convertImageIdsToImages(deletedImageIds, this.images);

this.images.addAll(addedImages);
this.images.removeAll(deletedImages);

return new BoardUpdateResult(addedImages, deletedImages);
}

public void validateWriter(final Long memberId) {
if (!this.writerId.equals(memberId)) {
throw new WriterNotEqualsException();
}
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/market/board/domain/board/BoardRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.market.board.domain.board;

import java.util.Optional;

public interface BoardRepository {

Board save(final Board board);

Optional<Board> findById(final Long id);

Optional<Board> findBoardWithImages(final Long boardId);

void deleteByBoardId(Long boardId);
}
35 changes: 35 additions & 0 deletions src/main/java/com/market/board/domain/board/BoardUpdateResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.market.board.domain.board;

import com.market.board.domain.image.Image;
import lombok.Getter;

import java.util.ArrayList;
import java.util.List;

@Getter
public class BoardUpdateResult {

private final List<Image> addedImages;
private final List<Image> deletedImages;

public BoardUpdateResult(final List<Image> addedImages, final List<Image> deletedImages) {
this.addedImages = handleAddedImages(addedImages);
this.deletedImages = handleDeletedImages(deletedImages);
}

private List<Image> handleAddedImages(final List<Image> addedImages) {
if (addedImages == null) {
return new ArrayList<>();
}

return addedImages;
}

private List<Image> handleDeletedImages(final List<Image> deletedImages) {
if (deletedImages == null) {
return new ArrayList<>();
}

return deletedImages;
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/market/board/domain/board/Post.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.market.board.domain.board;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Lob;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Post {

@Column(length = 32, nullable = false)
private String title;

@Lob
@Column(nullable = false)
private String content;

public static Post of(final String title, final String content) {
return new Post(title, content);
}

public void update(final String title, final String content) {
this.title = title;
this.content = content;
}
}