Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.recyclestudy.review.controller;

import com.recyclestudy.review.controller.request.ReviewSaveRequest;
import com.recyclestudy.review.controller.response.ReviewSaveResponse;
import com.recyclestudy.review.service.ReviewService;
import com.recyclestudy.review.service.input.ReviewSaveInput;
import com.recyclestudy.review.service.output.ReviewSaveOutput;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
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;

@RestController
@RequestMapping("/api/v1/reviews")
@RequiredArgsConstructor
public class ReviewController {

private final ReviewService reviewService;

@PostMapping
public ResponseEntity<ReviewSaveResponse> saveReview(@RequestBody ReviewSaveRequest request) {
final ReviewSaveInput input = request.toInput();
final ReviewSaveOutput output = reviewService.saveReview(input);
ReviewSaveResponse response = ReviewSaveResponse.of(output.url(), output.scheduledAts());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.recyclestudy.review.controller.request;

import com.recyclestudy.review.service.input.ReviewSaveInput;

public record ReviewSaveRequest(String identifier, String targetUrl) {

public ReviewSaveInput toInput() {
return ReviewSaveInput.of(this.identifier, this.targetUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.recyclestudy.review.controller.response;

import com.recyclestudy.review.domain.ReviewURL;
import java.time.LocalDateTime;
import java.util.List;

public record ReviewSaveResponse(String url, List<LocalDateTime> scheduledAts) {

public static ReviewSaveResponse of(ReviewURL url, List<LocalDateTime> scheduledAts) {
return new ReviewSaveResponse(url.getValue(), scheduledAts);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.recyclestudy.review.domain;

public enum NotificationStatus {
PENDING,
SENT,
FAILED,
;
}
38 changes: 38 additions & 0 deletions src/main/java/com/recyclestudy/review/domain/Review.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.recyclestudy.review.domain;

import com.recyclestudy.common.BaseEntity;
import com.recyclestudy.common.NullValidator;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldNameConstants;

@Entity
@Table(name = "review")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@FieldNameConstants(level = AccessLevel.PRIVATE)
@Getter
public class Review extends BaseEntity {

public static Review withoutId(final ReviewURL url) {
validateNotNull(url);
return new Review(url);
}

private static void validateNotNull(final ReviewURL url) {
NullValidator.builder()
.add(Fields.url, url)
.validate();
}

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "url", nullable = false, columnDefinition = "TEXT"))
private ReviewURL url;
}
58 changes: 58 additions & 0 deletions src/main/java/com/recyclestudy/review/domain/ReviewCycle.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.recyclestudy.review.domain;

import com.recyclestudy.common.BaseEntity;
import com.recyclestudy.common.NullValidator;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldNameConstants;

@Entity
@Table(name = "cycle")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@FieldNameConstants(level = AccessLevel.PRIVATE)
@Getter
public class ReviewCycle extends BaseEntity {

public static ReviewCycle withoutId(
final Review review,
final LocalDateTime scheduledAt,
final NotificationStatus status
) {
validateNotNull(review, scheduledAt, status);
return new ReviewCycle(review, scheduledAt, status);
}

private static void validateNotNull(
final Review review,
final LocalDateTime scheduledAt,
final NotificationStatus status
) {
NullValidator.builder()
.add(Fields.review, review)
.add(Fields.scheduledAt, scheduledAt)
.add(Fields.status, status)
.validate();
}

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id", nullable = false)
private Review review;

private LocalDateTime scheduledAt;

@Enumerated(value = EnumType.STRING)
@Column(name = "status", nullable = false)
private NotificationStatus status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.recyclestudy.review.domain;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;

public enum ReviewCycleDuration {
DAY(Duration.ofDays(1)),
WEEK(Duration.ofDays(7)),
MONTH(Duration.ofDays(30)),
QUARTER(Duration.ofDays(90)),
HALF_YEAR(Duration.ofDays(180)),
;

private final Duration duration;

ReviewCycleDuration(final Duration duration) {
this.duration = duration;
}

public static List<LocalDateTime> calculate(final LocalDate target, final LocalTime time) {
return Arrays.stream(ReviewCycleDuration.values())
.map(cycle -> target.plusDays(cycle.duration.toDays())
.atTime(time))
.toList();
}

public static List<LocalDateTime> calculate(final LocalDate target) {
return calculate(target, LocalTime.of(8, 0));
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/recyclestudy/review/domain/ReviewURL.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.recyclestudy.review.domain;

import com.recyclestudy.common.NullValidator;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@FieldNameConstants(level = AccessLevel.PRIVATE)
@Getter
@ToString
@EqualsAndHashCode
public class ReviewURL {

private String value;

public static ReviewURL from(final String value) {
validateNotNull(value);
return new ReviewURL(value);
}

private static void validateNotNull(final String value) {
NullValidator.builder()
.add(Fields.value, value)
.validate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.recyclestudy.review.repository;

import com.recyclestudy.review.domain.ReviewCycle;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReviewCycleRepository extends JpaRepository<ReviewCycle, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.recyclestudy.review.repository;

import com.recyclestudy.review.domain.Review;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReviewRepository extends JpaRepository<Review, Long> {
}
58 changes: 58 additions & 0 deletions src/main/java/com/recyclestudy/review/service/ReviewService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.recyclestudy.review.service;

import com.recyclestudy.exception.UnauthorizedException;
import com.recyclestudy.member.domain.Device;
import com.recyclestudy.member.repository.DeviceRepository;
import com.recyclestudy.review.domain.NotificationStatus;
import com.recyclestudy.review.domain.Review;
import com.recyclestudy.review.domain.ReviewCycle;
import com.recyclestudy.review.domain.ReviewCycleDuration;
import com.recyclestudy.review.repository.ReviewCycleRepository;
import com.recyclestudy.review.repository.ReviewRepository;
import com.recyclestudy.review.service.input.ReviewSaveInput;
import com.recyclestudy.review.service.output.ReviewSaveOutput;
import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ReviewService {

private final ReviewRepository reviewRepository;
private final ReviewCycleRepository reviewCycleRepository;
private final DeviceRepository deviceRepository;
private final Clock clock;

@Transactional
public ReviewSaveOutput saveReview(final ReviewSaveInput input) {
final Device device = deviceRepository.findByIdentifier(input.identifier())
.orElseThrow(() -> new UnauthorizedException("유효하지 않은 디바이스입니다"));
checkValidDevice(device);

final Review review = Review.withoutId(input.url());
final Review savedReview = reviewRepository.save(review);

final LocalDate current = LocalDate.now(clock);
final List<LocalDateTime> scheduledAts = ReviewCycleDuration.calculate(current);

final List<ReviewCycle> reviewCycles = scheduledAts.stream()
.map(scheduledAt -> ReviewCycle.withoutId(savedReview, scheduledAt, NotificationStatus.PENDING))
.toList();

final List<LocalDateTime> savedScheduledAts = reviewCycleRepository.saveAll(reviewCycles)
.stream().map(ReviewCycle::getScheduledAt)
.toList();
return ReviewSaveOutput.of(savedReview.getUrl(), savedScheduledAts);
}

private static void checkValidDevice(final Device device) {
if (!device.isActive()) {
throw new UnauthorizedException("인증되지 않은 디바이스입니다");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.recyclestudy.review.service.input;

import com.recyclestudy.member.domain.DeviceIdentifier;
import com.recyclestudy.review.domain.ReviewURL;

public record ReviewSaveInput(DeviceIdentifier identifier, ReviewURL url) {

public static ReviewSaveInput of(final String identifier, final String url) {
final DeviceIdentifier deviceIdentifier = DeviceIdentifier.from(identifier);
final ReviewURL reviewURL = ReviewURL.from(url);
return new ReviewSaveInput(deviceIdentifier, reviewURL);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.recyclestudy.review.service.output;

import com.recyclestudy.review.domain.ReviewURL;
import java.time.LocalDateTime;
import java.util.List;

public record ReviewSaveOutput(ReviewURL url, List<LocalDateTime> scheduledAts) {

public static ReviewSaveOutput of(ReviewURL url, List<LocalDateTime> scheduledAts) {
return new ReviewSaveOutput(url, scheduledAts);
}
}
Loading