Skip to content

Commit

Permalink
게시글 작성 기능 구현 (#69)
Browse files Browse the repository at this point in the history
* feat: (#32) Post 관련 객체들을 객체지향적으로 재구성

* refactor: (#32) 실무의 관례에 따라 엔티티의 일급 컬렉션을 필드에서 바로 초기화 하는 것으로 개선

* feat: (#32) swagger 이미지 파일 테스트가 가능하도록 환경 설정

* feat: (#32) TCP 소켓을 통해 접속해야 어플리케이션과 콘솔이 동시에 접근했을 때 오류가 발생하지 않도록 url 개선

* feat: (#32) 게시글 작성 API를 위한 계층 구조 구현

* test: (#32) 게시글 작성 기능 테스트 구현

* refactor: (#32) 클라이언트로부터 데이터를 전달받을 때 선택지 내용을 String 리스트로 받는 것으로 개선

* refactor: (#32) 더 안전한 사진으로 변경

* refactor: (#32) JavaTimeModule을 가져오기 위한 의존성 생략

* refactor: (#32) 빌더 생성자 private으로 개선

* refactor: (#32) Request Dto의 이름을 더 명확한 역할이 나타나도록 개선

* :refactor: (#32) PostCreateRequest를 record로 개선

* :refactor: (#32) 공백 정리

* refactor: (#32) final 키워드 붙이기

* refactor: (#32) test 관련 어노테이션의 위치 개선

* refactor: (#32) final 키워드 붙이기

* refactor: (#32) H2 DB를 인메모리 형식으로 변경

* refactor: (#32) 클라이언트로부터 받은 데이터로 엔티티를 만드는 과정 수정

* refactor: (#32) 클래스 마지막 줄 개행 추가

* refactor: (#32) 테스트 관련 어노테이션 위치 수정

* refactor: (#32) 테스트 주석 수정

* refactor: (#32) 접근 제어자 protected로 개선

* refactor: (#32) final 붙이기 및 타입 명시
  • Loading branch information
tjdtls690 authored Jul 18, 2023
1 parent b2e4690 commit b73b67f
Show file tree
Hide file tree
Showing 23 changed files with 731 additions and 26 deletions.
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

testImplementation 'io.rest-assured:rest-assured'
testImplementation 'io.rest-assured:spring-mock-mvc'

runtimeOnly 'com.h2database:h2'

compileOnly 'org.projectlombok:lombok'
Expand Down
18 changes: 18 additions & 0 deletions backend/src/main/java/com/votogether/config/SwaggerBeanConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.votogether.config;

import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

@Configuration
public class SwaggerBeanConfig {

public SwaggerBeanConfig(final MappingJackson2HttpMessageConverter converter) {
final List<MediaType> supportedMediaTypes = new ArrayList<>(converter.getSupportedMediaTypes());
supportedMediaTypes.add(new MediaType("application", "octet-stream"));
converter.setSupportedMediaTypes(supportedMediaTypes);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.votogether.domain.post.controller;

import com.votogether.domain.member.entity.Gender;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.entity.SocialType;
import com.votogether.domain.post.dto.request.PostCreateRequest;
import com.votogether.domain.post.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "게시글", description = "게시글 관련 API")
@RequiredArgsConstructor
@RequestMapping("/posts")
@RestController
public class PostController {

private final PostService postService;

@Operation(summary = "게시글 작성", description = "게시글을 저장한다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "게시물 생성되었습니다."),
@ApiResponse(responseCode = "400", description = "잘못된 입력입니다."),
@ApiResponse(responseCode = "500", description = "인터넷 서버 오류입니다.")
})
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> save(
@RequestPart final PostCreateRequest request,
@RequestPart final List<MultipartFile> images
) {
// TODO : 일단 돌아가게 하기 위한 member 저장 (실제 어플에선 삭제될 코드)
final Member member = Member.builder()
.socialType(SocialType.GOOGLE)
.socialId("tjdtls690")
.nickname("Abel")
.gender(Gender.MALE)
.point(100)
.birthDate(LocalDateTime.now())
.build();

final Long postId = postService.save(request, member, images);
return ResponseEntity.created(URI.create("/posts/" + postId)).build();
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.votogether.domain.post.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import lombok.Builder;
import org.springframework.format.annotation.DateTimeFormat;

@Schema(name = "게시글 관련 데이터", description = "게시글에 관련한 데이터들입니다.")
@Builder
public record PostCreateRequest (
List<Long> categoryIds,
String title,
String content,
List<String> postOptionContents,

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
LocalDateTime deadline
){

}
57 changes: 44 additions & 13 deletions backend/src/main/java/com/votogether/domain/post/entity/Post.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
package com.votogether.domain.post.entity;

import com.votogether.domain.category.entity.Category;
import com.votogether.domain.common.BaseEntity;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.vote.entity.Vote;
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.ManyToOne;
import jakarta.persistence.OneToMany;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
Expand All @@ -33,29 +35,58 @@ public class Post extends BaseEntity {
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Column(length = 100, nullable = false)
private String title;
@Embedded
private PostBody postBody;

@Column(length = 1000, nullable = false)
private String content;
@Embedded
private PostCategories postCategories;

@Embedded
private PostOptions postOptions;

@Column(columnDefinition = "datetime(2)", nullable = false)
private LocalDateTime deadline;

@OneToMany(mappedBy = "post")
private List<PostOption> postOptions = new ArrayList<>();

@Builder
private Post(
final Member member,
final String title,
final String content,
final PostBody postBody,
final LocalDateTime deadline
) {
this.member = member;
this.title = title;
this.content = content;
this.postBody = postBody;
this.deadline = deadline;
this.postCategories = new PostCategories();
this.postOptions = new PostOptions();
}

public void mapCategories(final List<Category> categories) {
this.postCategories.mapPostAndCategories(this, categories);
}

public void mapPostOptionsByElements(
final List<String> postOptionContents,
final Post post,
final List<MultipartFile> images
) {
this.postOptions.addAllPostOptions(toPostOptionEntities(postOptionContents, post, images));
}

private List<PostOption> toPostOptionEntities(
final List<String> postOptionContents,
final Post post,
final List<MultipartFile> images
) {
return IntStream.range(0, postOptionContents.size())
.mapToObj(postOptionSequence ->
PostOption.of(
postOptionContents.get(postOptionSequence),
post,
postOptionSequence,
images.get(postOptionSequence)
)
)
.toList();
}

public boolean hasPostOption(final PostOption postOption) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.votogether.domain.post.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Embeddable
public class PostBody {

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

@Column(length = 1000, nullable = false)
private String content;

@Builder
private PostBody(final String title, final String content) {
this.title = title;
this.content = content;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.votogether.domain.post.entity;

import com.votogether.domain.category.entity.Category;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Embeddable;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Embeddable
public class PostCategories {

@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<PostCategory> postCategories = new ArrayList<>();

public void mapPostAndCategories(final Post post, final List<Category> categories) {
categories.forEach(category -> postCategories.add(createPostCategory(post, category)));
}

private PostCategory createPostCategory(final Post post, final Category category) {
return PostCategory.builder()
.post(post)
.category(category)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
Expand All @@ -33,15 +39,59 @@ public class PostOption extends BaseEntity {
@Column(length = 50, nullable = false)
private String content;

@Column
private String imageUrl;

@Builder
private PostOption(
final Post post,
final Integer sequence,
final String content
final String content,
final String imageUrl
) {
this.post = post;
this.sequence = sequence;
this.content = content;
this.imageUrl = imageUrl;
}

public static PostOption of(
final String postOptionContent,
final Post post,
final int postOptionSequence,
final MultipartFile image
) {
if (!image.isEmpty()) {
final String imageUrl = saveImageToPath(image);
return toPostOptionEntity(post, postOptionSequence, postOptionContent, imageUrl);
}

return toPostOptionEntity(post, postOptionSequence, postOptionContent, "");
}

private static String saveImageToPath(final MultipartFile image) {
final String absolutePath = new File("").getAbsolutePath();
final String imageUrl = absolutePath + "/src/main/resources/images/" + image.getOriginalFilename();

try {
Files.write(Paths.get(imageUrl), image.getBytes());
} catch (IOException ignore) {
}
return imageUrl;
}

private static PostOption toPostOptionEntity(
final Post post,
final int postOptionSequence,
final String postOptionContent,
final String imageUrl
) {
return PostOption.builder()
.post(post)
.sequence(postOptionSequence)
.content(postOptionContent)
.imageUrl(imageUrl)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.votogether.domain.post.entity;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Embeddable;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Getter
@Embeddable
public class PostOptions {

@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<PostOption> postOptions = new ArrayList<>();

public void addAllPostOptions(final List<PostOption> postOptions) {
this.postOptions.addAll(postOptions);
}

public boolean contains(final PostOption postOption) {
return postOptions.contains(postOption);
}

}
Loading

0 comments on commit b73b67f

Please sign in to comment.