Skip to content

Commit

Permalink
서포터 피드백 등록 API 구현 (woowacourse-teams#186)
Browse files Browse the repository at this point in the history
* feat: Description VO 구현

* feat: SupporterFeedback 엔티티 구현

* chore: Feedback 관련 패키지 변경

* refactor: SupporterFeedback Entity 에 nullable 추가

* feat: Runner 에 equals&hashcode 추가

* feat: SupporterFeedback 등록 API 구현

* refactor: FeedbackBusinessException 내부 클래스 삭제

* test: 안 쓰는 인스턴스 변수 로컬 변수로 변경

* test: SupporterFeedback 관련 Fixture 구현

* test: RestAssuredTest 컨벤션에 맞게 변경

* refactor: FeedbackService 에 Transaction 추가

* refactor: FeedbackService Runner 찾는 과정 삭제

* refactor: 로그인 기능 추가에 따른 @AuthRunner 사용하도록 변경

* refactor: RunnerPost 와 SupporterFeedback 과의 관계를 OneToOne 으로 변경
  • Loading branch information
cookienc authored and eunbii0213 committed Aug 14, 2023
1 parent 571db35 commit 5ffa7aa
Show file tree
Hide file tree
Showing 26 changed files with 797 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public enum ClientErrorCode {
PAST_DEADLINE(HttpStatus.BAD_REQUEST, "RP006", "마감일은 오늘보다 과거일 수 없습니다."),
CONTENTS_NOT_FOUND(HttpStatus.NOT_FOUND, "RP007", "존재하지 않는 게시물입니다."),
TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP008", "태그 목록을 빈 값이라도 입력해주세요."),

REVIEW_TYPE_IS_NULL(HttpStatus.BAD_REQUEST, "FB001", "만족도를 입력해주세요."),
SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB002", "서포터 식별자를 입력해주세요."),
RUNNER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB003", "러너 식별자를 입력해주세요."),

COMPANY_IS_NULL(HttpStatus.BAD_REQUEST, "OM001", "사용자의 회사 정보를 입력해주세요."),
OAUTH_REQUEST_URL_PROVIDER_IS_WRONG(HttpStatus.BAD_REQUEST, "OA001", "redirect 할 url 이 조회되지 않는 잘못된 소셜 타입입니다."),
OAUTH_INFORMATION_CLIENT_IS_WRONG(HttpStatus.BAD_REQUEST, "OA002", " 소셜 계정 정보를 조회할 수 없는 잘못된 소셜 타입입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package touch.baton.domain.feedback;

import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import touch.baton.domain.common.BaseEntity;
import touch.baton.domain.feedback.exception.SupporterFeedbackException;
import touch.baton.domain.feedback.vo.Description;
import touch.baton.domain.feedback.vo.ReviewType;
import touch.baton.domain.runner.Runner;
import touch.baton.domain.runnerpost.RunnerPost;
import touch.baton.domain.supporter.Supporter;

import java.util.Objects;

import static jakarta.persistence.EnumType.STRING;
import static jakarta.persistence.FetchType.LAZY;
import static jakarta.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
public class SupporterFeedback extends BaseEntity {

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

@Enumerated(STRING)
@Column(nullable = false)
private ReviewType reviewType;

@Embedded
private Description description;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "supporter_id", nullable = false, foreignKey = @ForeignKey(name = "fk_supporter_feed_back_to_supporter"))
private Supporter supporter;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "runner_id", nullable = false, foreignKey = @ForeignKey(name = "fk_supporter_feed_back_to_runner"))
private Runner runner;

@OneToOne(fetch = LAZY)
@JoinColumn(name = "runner_post_id", nullable = false, foreignKey = @ForeignKey(name = "fk_supporter_feed_back_to_runner_post"))
private RunnerPost runnerPost;

@Builder
private SupporterFeedback(final ReviewType reviewType, final Description description, final Supporter supporter, final Runner runner, final RunnerPost runnerPost) {
this(null, reviewType, description, supporter, runner, runnerPost);
}

private SupporterFeedback(final Long id, final ReviewType reviewType, final Description description, final Supporter supporter, final Runner runner, final RunnerPost runnerPost) {
validateNotNull(reviewType, description, supporter, runner, runnerPost);
this.id = id;
this.reviewType = reviewType;
this.description = description;
this.supporter = supporter;
this.runner = runner;
this.runnerPost = runnerPost;
}

private void validateNotNull(final ReviewType reviewType,
final Description description,
final Supporter supporter,
final Runner runner,
final RunnerPost runnerPost
) {
if (Objects.isNull(reviewType)) {
throw new SupporterFeedbackException("SupporterFeedback 의 reviewType 은 null 일 수 없습니다.");
}

if (Objects.isNull(description)) {
throw new SupporterFeedbackException("SupporterFeedback 의 description 은 null 일 수 없습니다.");
}

if (Objects.isNull(supporter)) {
throw new SupporterFeedbackException("SupporterFeedback 의 supporter 는 null 일 수 없습니다.");
}

if (Objects.isNull(runner)) {
throw new SupporterFeedbackException("SupporterFeedback 의 runner 는 null 일 수 없습니다.");
}

if (Objects.isNull(runnerPost)) {
throw new SupporterFeedbackException("SupporterFeedback 의 runnerPost 는 null 일 수 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package touch.baton.domain.feedback.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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 org.springframework.web.util.UriComponentsBuilder;
import touch.baton.domain.feedback.service.FeedbackService;
import touch.baton.domain.feedback.service.SupporterFeedBackCreateRequest;
import touch.baton.domain.oauth.controller.resolver.AuthRunnerPrincipal;
import touch.baton.domain.runner.Runner;
import touch.baton.domain.runner.service.RunnerService;

import java.net.URI;

@RequiredArgsConstructor
@RequestMapping("/api/v1/feedback")
@RestController
public class FeedbackController {

private final FeedbackService feedbackService;
private final RunnerService runnerService;

@PostMapping("/supporter")
public ResponseEntity<Void> createSupporterFeedback(@AuthRunnerPrincipal final Runner runner,
@Valid @RequestBody final SupporterFeedBackCreateRequest request
) {
final Long savedId = feedbackService.createSupporterFeedback(runner, request);

final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/feedback/supporter")
.path("/{id}")
.buildAndExpand(savedId)
.toUri();
return ResponseEntity.created(redirectUri).build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package touch.baton.domain.feedback.exception;

import touch.baton.domain.common.exception.BusinessException;

public class FeedbackBusinessException extends BusinessException {

public FeedbackBusinessException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package touch.baton.domain.feedback.exception;

import touch.baton.domain.common.exception.DomainException;

public class SupporterFeedbackException extends DomainException {

public SupporterFeedbackException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package touch.baton.domain.feedback.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import touch.baton.domain.feedback.SupporterFeedback;

public interface SupporterFeedbackRepository extends JpaRepository<SupporterFeedback, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package touch.baton.domain.feedback.service;


import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import touch.baton.domain.feedback.SupporterFeedback;
import touch.baton.domain.feedback.exception.FeedbackBusinessException;
import touch.baton.domain.feedback.repository.SupporterFeedbackRepository;
import touch.baton.domain.feedback.vo.Description;
import touch.baton.domain.feedback.vo.ReviewType;
import touch.baton.domain.runner.Runner;
import touch.baton.domain.runnerpost.RunnerPost;
import touch.baton.domain.runnerpost.repository.RunnerPostRepository;
import touch.baton.domain.supporter.Supporter;
import touch.baton.domain.supporter.repository.SupporterRepository;

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

private static final String DELIMITER = "|";

private final SupporterFeedbackRepository supporterFeedbackRepository;
private final RunnerPostRepository runnerPostRepository;
private final SupporterRepository supporterRepository;

@Transactional
public Long createSupporterFeedback(final Runner runner, final SupporterFeedBackCreateRequest request) {
final Supporter foundSupporter = supporterRepository.findById(request.supporterId())
.orElseThrow(() -> new FeedbackBusinessException("서포터를 찾을 수 없습니다."));
final RunnerPost foundRunnerPost = runnerPostRepository.findById(request.runnerPostId())
.orElseThrow(() -> new FeedbackBusinessException("러너 게시글을 찾을 수 없습니다."));

if (foundRunnerPost.isNotOwner(runner)) {
throw new FeedbackBusinessException("리뷰 글을 작성한 주인만 글을 작성할 수 있습니다.");
}

if (foundRunnerPost.isDifferentSupporter(foundSupporter)) {
throw new FeedbackBusinessException("리뷰를 작성한 서포터에 대해서만 피드백을 작성할 수 있습니다.");
}

final SupporterFeedback supporterFeedback = SupporterFeedback.builder()
.reviewType(ReviewType.valueOf(request.reviewType()))
.description(new Description(String.join(DELIMITER, request.descriptions())))
.runner(runner)
.supporter(foundSupporter)
.runnerPost(foundRunnerPost)
.build();

return supporterFeedbackRepository.save(supporterFeedback).getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package touch.baton.domain.feedback.service;

import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.common.exception.validator.ValidNotNull;

import java.util.List;

public record SupporterFeedBackCreateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.REVIEW_TYPE_IS_NULL)
String reviewType,
List<String> descriptions,
@ValidNotNull(clientErrorCode = ClientErrorCode.SUPPORTER_ID_IS_NULL)
Long supporterId,
@ValidNotNull(clientErrorCode = ClientErrorCode.RUNNER_ID_IS_NULL)
Long runnerPostId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package touch.baton.domain.feedback.vo;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Objects;

import static lombok.AccessLevel.PROTECTED;

@EqualsAndHashCode
@Getter
@NoArgsConstructor(access = PROTECTED)
@Embeddable
public class Description {

@Column(name = "description", nullable = true, columnDefinition = "TEXT")
private String value;

public Description(final String value) {
validateNotNull(value);
this.value = value;
}

private void validateNotNull(final String value) {
if (Objects.isNull(value)) {
throw new IllegalArgumentException("Description 객체 내부에 value 는 null 일 수 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package touch.baton.domain.feedback.vo;

public enum ReviewType {

GREAT, GOOD, BAD
}
13 changes: 13 additions & 0 deletions backend/baton/src/main/java/touch/baton/domain/runner/Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,17 @@ private void validateNotNull(final TotalRating totalRating, final Grade grade, f
throw new OldRunnerException.NotNull("member 는 null 일 수 없습니다.");
}
}

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

@Override
public int hashCode() {
return Objects.hash(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ public void increaseWatchedCount() {
this.watchedCount = watchedCount.increase();
}

public boolean isNotOwner(final Runner targetRunner) {
return !runner.equals(targetRunner);
}

public boolean isDifferentSupporter(final Supporter targetSupporter) {
return !supporter.equals(targetSupporter);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,17 @@ private void validateNotNull(final ReviewCount reviewCount,
public void addAllSupporterTechnicalTags(final List<SupporterTechnicalTag> supporterTechnicalTags) {
this.supporterTechnicalTags.addAll(supporterTechnicalTags);
}

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

@Override
public int hashCode() {
return Objects.hash(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@

public class AssuredSupport {

public static ExtractableResponse<Response> post(final String uri, final Object params) {
return RestAssured
.given().log().ifValidationFails()
.contentType(APPLICATION_JSON_VALUE)
.body(params)
.when().log().ifValidationFails()
.post(uri)
.then().log().ifError()
.extract();
}

public static ExtractableResponse<Response> get(final String uri, final String pathParamName, final Long id) {
return RestAssured
.given().log().ifValidationFails()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package touch.baton.assure.common;

import org.springframework.http.HttpStatus;

public class HttpStatusAndLocationHeader {

private final HttpStatus httpStatus;
private final String location;

public HttpStatusAndLocationHeader(final HttpStatus httpStatus, final String location) {
this.httpStatus = httpStatus;
this.location = location;
}

public HttpStatus getHttpStatus() {
return httpStatus;
}

public String getLocation() {
return location;
}
}
Loading

0 comments on commit 5ffa7aa

Please sign in to comment.