Skip to content

Commit

Permalink
[JT-40] 회차 구매 API 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
kmebin committed Sep 11, 2023
2 parents 2946a0d + 67694e8 commit 056b8df
Show file tree
Hide file tree
Showing 20 changed files with 470 additions and 187 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ project(':module-domain') {
bootJar.enabled = false

dependencies {
implementation project(':module-domain-s3')
implementation project(':module-domain-smtp')
implementation project(':module-internal')
implementation project(':module-core')
Expand All @@ -62,6 +61,7 @@ project(':module-domain-s3') {
bootJar.enabled = false

dependencies {
implementation project(':module-internal')
implementation project(':module-core')
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.devtoon.jtoon.webtoon.application;

import static com.devtoon.jtoon.common.ImageType.*;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.devtoon.jtoon.application.S3Service;
import com.devtoon.jtoon.common.FileName;
import com.devtoon.jtoon.global.util.CustomPageRequest;
import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.request.UploadImageReq;
import com.devtoon.jtoon.webtoon.entity.Webtoon;
import com.devtoon.jtoon.webtoon.entity.enums.DayOfWeek;
import com.devtoon.jtoon.webtoon.request.CreateEpisodeReq;
import com.devtoon.jtoon.webtoon.request.CreateWebtoonReq;
import com.devtoon.jtoon.webtoon.request.GetWebtoonsReq;
import com.devtoon.jtoon.webtoon.response.EpisodeRes;
import com.devtoon.jtoon.webtoon.response.EpisodesRes;
import com.devtoon.jtoon.webtoon.response.WebtoonInfoRes;
import com.devtoon.jtoon.webtoon.response.WebtoonItemRes;
import com.devtoon.jtoon.webtoon.service.WebtoonDomainService;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class WebtoonApplicationService {

private final WebtoonDomainService webtoonDomainService;
private final S3Service s3Service;

@Transactional
public void createWebtoon(Member member, MultipartFile thumbnailImage, CreateWebtoonReq request) {
webtoonDomainService.validateDuplicateTitle(request.title());
String thumbnailUrl = s3Service.upload(UploadImageReq.builder()
.imageType(WEBTOON_THUMBNAIL)
.webtoonTitle(request.title())
.fileName(FileName.forWebtoon())
.image(thumbnailImage)
.build()
);
webtoonDomainService.createWebtoon(member, thumbnailUrl, request);
}

@Transactional
public void createEpisode(
Member member,
Long webtoonId,
MultipartFile mainImage,
MultipartFile thumbnailImage,
CreateEpisodeReq request
) {
Webtoon webtoon = webtoonDomainService.getWebtoonById(webtoonId);
webtoon.validateAuthor(member.getId());
String mainUrl = s3Service.upload(UploadImageReq.builder()
.imageType(EPISODE_MAIN)
.webtoonTitle(webtoon.getTitle())
.fileName(FileName.forEpisode(request.no()))
.image(mainImage)
.build()
);
String thumbnailUrl = s3Service.upload(UploadImageReq.builder()
.imageType(EPISODE_THUMBNAIL)
.webtoonTitle(webtoon.getTitle())
.fileName(FileName.forEpisode(request.no()))
.image(thumbnailImage)
.build()
);
webtoonDomainService.createEpisode(webtoon, mainUrl, thumbnailUrl, request);
}

public Map<DayOfWeek, List<WebtoonItemRes>> getWebtoons(GetWebtoonsReq request) {
return webtoonDomainService.getWebtoons(request);
}

public WebtoonInfoRes getWebtoon(Long webtoonId) {
return webtoonDomainService.getWebtoon(webtoonId);
}

public List<EpisodesRes> getEpisodes(Long webtoonId, CustomPageRequest request) {
return webtoonDomainService.getEpisodes(webtoonId, request);
}

public EpisodeRes getEpisode(Long episodeId) {
return webtoonDomainService.getEpisode(episodeId);
}

@Transactional
public void purchaseEpisode(Member member, Long episodeId) {
webtoonDomainService.purchaseEpisode(member, episodeId);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.devtoon.jtoon.webtoon.presentation;

import java.util.List;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -14,27 +17,25 @@
import com.devtoon.jtoon.global.util.CustomPageRequest;
import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.security.domain.jwt.MemberThreadLocal;
import com.devtoon.jtoon.webtoon.application.WebtoonService;
import com.devtoon.jtoon.webtoon.application.WebtoonApplicationService;
import com.devtoon.jtoon.webtoon.entity.enums.DayOfWeek;
import com.devtoon.jtoon.webtoon.request.CreateEpisodeReq;
import com.devtoon.jtoon.webtoon.request.CreateWebtoonReq;
import com.devtoon.jtoon.webtoon.request.GetEpisodeReq;
import com.devtoon.jtoon.webtoon.request.GetWebtoonsReq;
import com.devtoon.jtoon.webtoon.response.EpisodeRes;
import com.devtoon.jtoon.webtoon.response.EpisodesRes;
import com.devtoon.jtoon.webtoon.response.WebtoonInfoRes;
import com.devtoon.jtoon.webtoon.response.WebtoonItemRes;

import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/webtoons")
public class WebtoonController {

private final WebtoonService webtoonService;
private final WebtoonApplicationService webtoonService;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Expand All @@ -47,12 +48,12 @@ public void createWebtoon(@RequestPart MultipartFile thumbnailImage, @RequestPar
@ResponseStatus(HttpStatus.CREATED)
public void createEpisode(
@PathVariable Long webtoonId,
@RequestPart @Valid CreateEpisodeReq request,
@RequestPart MultipartFile mainImage,
@RequestPart(required = false) MultipartFile thumbnailImage
@RequestPart(required = false) MultipartFile thumbnailImage,
@RequestPart @Valid CreateEpisodeReq request
) {
Member member = MemberThreadLocal.getMember();
webtoonService.createEpisode(member, webtoonId, request, mainImage, thumbnailImage);
webtoonService.createEpisode(member, webtoonId, mainImage, thumbnailImage, request);
}

@GetMapping
Expand All @@ -72,9 +73,14 @@ public List<EpisodesRes> getEpisodes(@RequestParam Long webtoonId, CustomPageReq
return webtoonService.getEpisodes(webtoonId, request);
}

@GetMapping("/detail")
@ResponseStatus(HttpStatus.OK)
public EpisodeRes detailEpisode(@RequestParam Long episodeId, @RequestParam GetEpisodeReq request) {
return webtoonService.getDetailEpisode(episodeId, request);
@GetMapping("/episodes/{episodeId}")
public EpisodeRes getEpisode(@PathVariable Long episodeId) {
return webtoonService.getEpisode(episodeId);
}

@PostMapping("/episodes/{episodeId}/purchase")
public void purchaseEpisode(@PathVariable Long episodeId) {
Member member = MemberThreadLocal.getMember();
webtoonService.purchaseEpisode(member, episodeId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.devtoon.jtoon.common.FileName;
import com.devtoon.jtoon.common.ImageType;
import com.devtoon.jtoon.request.UploadImageReq;
import com.devtoon.jtoon.util.S3Uploader;

import lombok.RequiredArgsConstructor;
Expand All @@ -19,9 +17,9 @@ public class S3Service {
@Value("${spring.cloud.aws.cloud-front.url}")
private String IMAGE_URL;

public String upload(ImageType imageType, String webtoonTitle, FileName fileName, MultipartFile image) {
String key = imageType.getPath(webtoonTitle, fileName.getValue());
s3Uploader.upload(key, image);
public String upload(UploadImageReq request) {
String key = request.toKey();
s3Uploader.upload(key, request.image());

return IMAGE_URL + key;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.devtoon.jtoon.request;

import org.springframework.web.multipart.MultipartFile;

import com.devtoon.jtoon.common.FileName;
import com.devtoon.jtoon.common.ImageType;

import lombok.Builder;

@Builder
public record UploadImageReq(
ImageType imageType,
String webtoonTitle,
FileName fileName,
MultipartFile image
) {

public String toKey() {
return imageType.getPath(webtoonTitle, fileName.getValue());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.devtoon.jtoon.util;

import static com.devtoon.jtoon.error.model.ErrorStatus.*;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import com.devtoon.jtoon.error.exception.InvalidRequestException;

import io.awspring.cloud.s3.ObjectMetadata;
import io.awspring.cloud.s3.S3Template;
import lombok.RequiredArgsConstructor;
Expand All @@ -28,7 +32,7 @@ public void upload(String key, MultipartFile file) {
ObjectMetadata.builder().contentType("image/png").build()
);
} catch (IOException e) {
throw new RuntimeException(e);
throw new InvalidRequestException(S3_UPLOAD_FAIL);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.devtoon.jtoon.member.entity;

import static com.devtoon.jtoon.error.model.ErrorStatus.*;
import static java.util.Objects.*;

import com.devtoon.jtoon.error.exception.InvalidRequestException;
import com.devtoon.jtoon.global.common.BaseTimeEntity;

import jakarta.persistence.Column;
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.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "member_cookies")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberCookie extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_cookie_id")
private Long id;

@Column(name = "cookie_count", nullable = false)
private int cookieCount;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Builder
private MemberCookie(int cookieCount, Member member) {
if (cookieCount < 0) {
throw new InvalidRequestException(COOKIE_COUNT_NOT_NEGATIVE);
}

this.cookieCount = cookieCount;
this.member = requireNonNull(member, MEMBER_IS_NULL.getMessage());
}

public void decreaseCookieCount(int cookieCount) {
if (this.cookieCount < cookieCount) {
throw new InvalidRequestException(EPISODE_NOT_ENOUGH_COOKIES);
}

this.cookieCount -= cookieCount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.devtoon.jtoon.member.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.devtoon.jtoon.member.entity.MemberCookie;

public interface MemberCookieRepository extends JpaRepository<MemberCookie, Long> {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.devtoon.jtoon.webtoon.entity;

import static com.devtoon.jtoon.error.model.ErrorStatus.*;
import static java.util.Objects.*;

import com.devtoon.jtoon.global.common.BaseTimeEntity;
Expand Down Expand Up @@ -42,7 +43,7 @@ public class DayOfWeekWebtoon extends BaseTimeEntity {

@Builder
private DayOfWeekWebtoon(DayOfWeek dayOfWeek, Webtoon webtoon) {
this.dayOfWeek = requireNonNull(dayOfWeek, "dayOfWeek is null");
this.webtoon = requireNonNull(webtoon, "webtoon is null");
this.dayOfWeek = requireNonNull(dayOfWeek, WEBTOON_DAY_OF_WEEK_IS_NULL.getMessage());
this.webtoon = requireNonNull(webtoon, WEBTOON_IS_NULL.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.devtoon.jtoon.webtoon.entity;

import static com.devtoon.jtoon.error.model.ErrorStatus.*;
import static java.util.Objects.*;

import java.time.LocalDateTime;

import org.hibernate.annotations.ColumnDefault;

import com.devtoon.jtoon.error.exception.InvalidRequestException;
import com.devtoon.jtoon.global.common.BaseTimeEntity;

import jakarta.persistence.Column;
Expand Down Expand Up @@ -69,15 +71,19 @@ private Episode(
Webtoon webtoon
) {
if (no <= 0) {
throw new RuntimeException("number is zero or negative number");
throw new InvalidRequestException(EPISODE_NUMBER_POSITIVE);
}

this.no = no;
this.title = requireNonNull(title, "title is null");
this.mainUrl = requireNonNull(mainUrl, "mainUrl is null");
this.title = requireNonNull(title, EPISODE_TITLE_IS_NULL.getMessage());
this.mainUrl = requireNonNull(mainUrl, EPISODE_MAIN_URL_IS_NULL.getMessage());
this.thumbnailUrl = thumbnailUrl;
this.hasComment = hasComment;
this.openedAt = requireNonNull(openedAt, "openedAt is null");
this.webtoon = requireNonNull(webtoon, "webtoon is null");
this.openedAt = requireNonNull(openedAt, EPISODE_OPENED_AT_IS_NULL.getMessage());
this.webtoon = requireNonNull(webtoon, WEBTOON_IS_NULL.getMessage());
}

public int getCookieCount() {
return webtoon.getCookieCount();
}
}
Loading

0 comments on commit 056b8df

Please sign in to comment.