Skip to content

Commit

Permalink
Feat/#134 파트에 댓글 작성 기능 추가 (#146)
Browse files Browse the repository at this point in the history
* feat: 파트에 댓글 작성, 최신 순으로 조회 기능 구현

* refactor: BE 코드리뷰 반영

reviewer : somsom13

* refactor: RestAssured 응답 본문 추출 로직 변경

TypeRef 사용에서 jsonPath().getList() 로 변경

* refactor: 사용하지 않는 PartCommentsResponse 제거

* refactor: BE 코드리뷰 반영

Reviewed-by: seokhwan-an

* Feat/#123 프론트엔드 테스트 CI (lint, test, build) (#129)

* config: 프론트엔드 ci 설정

- lint
- test
- build

* style: 주석 삭제

* config: frontend 폴더에서만 trigger 되도록 설정

* config: pull request types 추가

* config: jobs 통합

* config: 프론트엔드 테스트 실패 슬랙 알림

* config: node version lts/hydrogen(18.16.1)

* fix: npm ci 실행 step 추가 (#137)

* fix: working directory 작성 및 if 절 순서 변경 (#138)

* Feat/#120 .env 환경 변수 설정 및 MSW 설정 (#132)

* config: dotenv설치 및 환경변수 설정

* config: msw 라이브러리 설치 및 적용

* refactor: msw 동적 import 로직 간결하게 변경

* config: package-lock 업데이트 및 script에 NODE_ENV 환경변수

* refactor: DefinePlugin common에서 공통적으로 적용

* feat: msw 파일 구조 변경 및 handler 실패시에도 response

* config: package-lock.json 최신화

---------

Co-authored-by: cruelladevil <dev.timetravel@gmail.com>

* Fix/#141 폴더 상관없이 항상 CI를 실행하도록 변경 (#142)

* config: paths 속성 삭제

- 폴더를 기준으로 트리거되던 action들을 무조건 실행하도록 변경

* config: node-version matrix strategy를 env 환경 변수로 변경

* Refactor/#121 페이지 레이아웃 및 반응형 디자인 통일 (#135)

* chore: 로고 아이콘 디렉터리 이동

* design: 헤더 높이 theme 추가

* design: 레이아웃 및 헤더 컴포넌트의 박스 사이징 조정

1. 헤더 높이에 맞게 main content 높이 조정
2. 로고 반응형 추가
3. 박스 사이징 시안에 맞게 수정

* design: 레이아웃 min-height 로 변경

컨텐츠가 높이를 넘어갔을 때 자연스럽게 표시되도록 수정

* design: 유튜브 영상 화면비 16:9로 반응형 구현

* design: 토글 그룹 spacing 삭제

* chore: 더미 앨범 자켓 이미지 추가

* design: 킬링파트 등록 페이지 디자인 조정

1. 더미 엘범 자켓 태그추가
2. spacing 세부 조정
3. 슬라이더 / 인터벌 인풋 위치 변경
4. 텍스트 반응형 디자인 추가

* design: theme 미적용된 속성에 theme 일괄 적용

* design: 인터벌 인풋 엑티브 색상 제거

* fix: jest 컴포넌트 랜더링 시 ThemeProvider를 제공하도록 수정

기존 render함수를 래핑한 renderWithTheme 함수 추가 및 적용

* config: 스토리북에 theme관련 설정 추가

1. 스토리북 themeProvider 적용
2. 스토리북 디렉터리 하위에도 tsc 적용되도록 설정 추가

---------

Co-authored-by: 윤정민 <dev.timetravel@gmail.com>

* config: 윈도우 환경변수 이슈 해결 (#155)

1. script에서 주입하던 NODE_ENV 환경변수 삭제
2. webpack.common.js 파일에서 분기로 처리하던 dotenv path 관련 로직 삭제
3. dev, prod 설정 파일 내 dotenv path 적용

* Feat/#144: 총 득표수가 높은 상위 40개 노래 목록 조회 기능 구현 (#159)

* feat: 총 득표수가 높은 상위 40개 노래 목록 조회 기능 구현

* feat: Song 에 imageUrl 필드 추가

* refactor: BE 코드리뷰 반영

Reviewed-by: seokhwan-an, somsom13, Cyma-s

* refactor: BE 코드리뷰 추가 반영

* Feat/#167: 백엔드 CD 설정 추가 (#168)

* config: 백엔드 CD 설정 추가

* chore: 백엔드 CD 파일 이름 변경

* hotfix: 백엔드 CD pwd 로깅 추가 (#169)

* config: 입력 받은 브랜치 CD 실행하도록 변경 (#170)

* hotfix: 따옴표 삭제 (#171)

* config: 실행 권한 root 로 지정 (#173)

* config: sudo 권한 삭제 (#174)

* fix: application.yml 수정 (#181)

* refactor: BE 코드리뷰 반영

Reviewed-by: somsom13, Cyma-s

* refactor: Repository 어노테이션 추가

---------

Co-authored-by: 윤정민 <dev.timetravel@gmail.com>
Co-authored-by: ukkodeveloper <ukkodeveloper@gmail.com>
Co-authored-by: 이도현 <77152650+Creative-Lee@users.noreply.github.com>
Co-authored-by: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com>
  • Loading branch information
5 people authored Aug 2, 2023
1 parent c95957e commit d045531
Show file tree
Hide file tree
Showing 19 changed files with 808 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package shook.shook.part.application;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import shook.shook.part.application.dto.PartCommentRegisterRequest;
import shook.shook.part.application.dto.PartCommentResponse;
import shook.shook.part.domain.Part;
import shook.shook.part.domain.PartComment;
import shook.shook.part.domain.repository.PartCommentRepository;
import shook.shook.part.domain.repository.PartRepository;
import shook.shook.part.exception.PartException;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class PartCommentService {

private final PartRepository partRepository;
private final PartCommentRepository partCommentRepository;

@Transactional
public void register(final Long partId, final PartCommentRegisterRequest request) {
final Part part = partRepository.findById(partId)
.orElseThrow(PartException.PartNotExistException::new);
final PartComment partComment = PartComment.forSave(part, request.getContent());

part.addComment(partComment);
partCommentRepository.save(partComment);
}

public List<PartCommentResponse> findPartReplies(final Long partId) {
final Part part = partRepository.findById(partId)
.orElseThrow(PartException.PartNotExistException::new);

return PartCommentResponse.getList(part.getCommentsInRecentOrder());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package shook.shook.part.application.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
@Getter
public class PartCommentRegisterRequest {

private String content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package shook.shook.part.application.dto;

import java.time.LocalDateTime;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import shook.shook.part.domain.PartComment;

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class PartCommentResponse {

private final Long id;
private final String content;
private final LocalDateTime createdAt;

public static PartCommentResponse from(final PartComment partComment) {
return new PartCommentResponse(
partComment.getId(),
partComment.getContent(),
partComment.getCreatedAt()
);
}

public static List<PartCommentResponse> getList(final List<PartComment> partComments) {
return partComments.stream()
.map(PartCommentResponse::from)
.toList();
}
}
20 changes: 20 additions & 0 deletions backend/src/main/java/shook/shook/part/domain/Part.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package shook.shook.part.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand All @@ -22,6 +23,7 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import shook.shook.part.exception.PartCommentException;
import shook.shook.part.exception.PartException;
import shook.shook.part.exception.VoteException;
import shook.shook.song.domain.Song;
Expand Down Expand Up @@ -54,6 +56,9 @@ public class Part {
@OneToMany(mappedBy = "part")
private final List<Vote> votes = new ArrayList<>();

@Embedded
private final PartComments comments = new PartComments();

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();

Expand Down Expand Up @@ -118,6 +123,13 @@ private void validateVote(final Vote vote) {
}
}

public void addComment(final PartComment comment) {
if (comment.isBelongToOtherPart(this)) {
throw new PartCommentException.CommentForOtherPartException();
}
comments.addComment(comment);
}

public boolean hasEqualStartAndLength(final Part other) {
return this.startSecond == other.startSecond && this.length.equals(other.length);
}
Expand All @@ -143,6 +155,14 @@ public int getVoteCount() {
return votes.size();
}

public List<PartComment> getComments() {
return comments.getComments();
}

public List<PartComment> getCommentsInRecentOrder() {
return comments.getCommentsInRecentOrder();
}

@Override
public boolean equals(final Object o) {
if (this == o) {
Expand Down
89 changes: 89 additions & 0 deletions backend/src/main/java/shook/shook/part/domain/PartComment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package shook.shook.part.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "part_comment")
@Entity
public class PartComment {

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

@Embedded
private PartCommentContent content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "part_id", foreignKey = @ForeignKey(name = "none"), nullable = false, updatable = false)
@Getter(AccessLevel.NONE)
private Part part;

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();

@PrePersist
private void prePersist() {
createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS);
}

private PartComment(final Long id, final Part part, final String content) {
this.id = id;
this.part = part;
this.content = new PartCommentContent(content);
}

public static PartComment saved(final Long id, final Part part, final String content) {
return new PartComment(id, part, content);
}

public static PartComment forSave(final Part part, final String content) {
return new PartComment(null, part, content);
}

public boolean isBelongToOtherPart(final Part part) {
return !this.part.equals(part);
}

public String getContent() {
return content.getValue();
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final PartComment partComment = (PartComment) o;
if (Objects.isNull(partComment.id) || Objects.isNull(this.id)) {
return false;
}
return Objects.equals(id, partComment.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package shook.shook.part.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import shook.shook.part.exception.PartCommentException;
import shook.shook.util.StringChecker;

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

private static final int MAXIMUM_LENGTH = 200;

@Column(name = "content", length = 200, nullable = false)
private String value;

public PartCommentContent(final String value) {
validate(value);
this.value = value;
}

private void validate(final String value) {
if (StringChecker.isNullOrBlank(value)) {
throw new PartCommentException.NullOrEmptyPartCommentException();
}
if (value.length() > MAXIMUM_LENGTH) {
throw new PartCommentException.TooLongPartCommentException();
}
}
}
41 changes: 41 additions & 0 deletions backend/src/main/java/shook/shook/part/domain/PartComments.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package shook.shook.part.domain;

import jakarta.persistence.Embeddable;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import shook.shook.part.exception.PartCommentException;

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

@OneToMany(mappedBy = "part")
private final List<PartComment> comments = new ArrayList<>();

public void addComment(final PartComment comment) {
validateComment(comment);
comments.add(comment);
}

private void validateComment(final PartComment comment) {
if (comments.contains(comment)) {
throw new PartCommentException.DuplicateCommentExistException();
}
}

public List<PartComment> getComments() {
return new ArrayList<>(comments);
}

public List<PartComment> getCommentsInRecentOrder() {
return comments.stream()
.sorted(Comparator.comparing(PartComment::getCreatedAt).reversed())
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package shook.shook.part.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import shook.shook.part.domain.PartComment;

@Repository
public interface PartCommentRepository extends JpaRepository<PartComment, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package shook.shook.part.exception;

public class PartCommentException extends RuntimeException {

public static class NullOrEmptyPartCommentException extends PartCommentException {

public NullOrEmptyPartCommentException() {
super();
}
}

public static class TooLongPartCommentException extends PartCommentException {

public TooLongPartCommentException() {
super();
}
}

public static class CommentForOtherPartException extends PartCommentException {

public CommentForOtherPartException() {
super();
}
}

public static class DuplicateCommentExistException extends PartCommentException {

public DuplicateCommentExistException() {
super();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package shook.shook.part.ui;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import shook.shook.part.application.PartCommentService;
import shook.shook.part.application.dto.PartCommentRegisterRequest;
import shook.shook.part.application.dto.PartCommentResponse;

@RequiredArgsConstructor
@RequestMapping("/songs/{song_id}/parts/{part_id}/comments")
@RestController
public class PartCommentController {

private final PartCommentService partCommentService;

@PostMapping
public ResponseEntity<Void> registerPartComment(
@PathVariable(name = "part_id") final Long partId,
@RequestBody final PartCommentRegisterRequest request
) {
partCommentService.register(partId, request);

return ResponseEntity.status(HttpStatus.CREATED).build();
}

@GetMapping
public ResponseEntity<List<PartCommentResponse>> findPartReplies(
@PathVariable(name = "part_id") final Long partId
) {
return ResponseEntity.ok(partCommentService.findPartReplies(partId));
}
}
Loading

0 comments on commit d045531

Please sign in to comment.