Skip to content

Commit 111f0dd

Browse files
Merge pull request #615 from Podo-Store/develop
[REFACTOR] 결제, 환불
2 parents db0eb34 + c10e76f commit 111f0dd

12 files changed

Lines changed: 301 additions & 144 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package PodoeMarket.podoemarket.common.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.LocalDateTime;
10+
import java.time.ZoneId;
11+
import java.util.UUID;
12+
13+
@Entity
14+
@Table(
15+
name = "pdfDownloadLog",
16+
uniqueConstraints = {
17+
@UniqueConstraint(columnNames = {"order_item_id"})
18+
}
19+
)
20+
@Data
21+
@Builder
22+
@AllArgsConstructor
23+
@NoArgsConstructor
24+
public class PdfDownloadLogEntity {
25+
@Id
26+
@GeneratedValue(strategy = GenerationType.IDENTITY)
27+
private Long id;
28+
29+
@Column(name = "order_item_id", nullable = false)
30+
private UUID orderItemId;
31+
32+
@Column(name = "user_id",nullable = false)
33+
private UUID userId;
34+
35+
@Column(nullable = false)
36+
private LocalDateTime downloadedAt;
37+
38+
@PrePersist
39+
protected void onCreate() {
40+
downloadedAt = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
41+
}
42+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package PodoeMarket.podoemarket.common.entity.type;
2+
3+
public enum BuyOption {
4+
PERFORMANCE,
5+
SCRIPT
6+
}

src/main/java/PodoeMarket/podoemarket/common/repository/OrderItemRepository.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.data.jpa.repository.Query;
1010
import org.springframework.data.repository.query.Param;
1111

12+
import java.time.LocalDateTime;
1213
import java.util.List;
1314
import java.util.UUID;
1415

@@ -71,8 +72,10 @@ SELECT COALESCE(SUM(oi.performanceAmount), 0)
7172
WHERE p.title LIKE %:keyword%
7273
OR p.writer LIKE %:keyword%
7374
OR u.nickname LIKE %:keyword%
74-
""")
75+
""")
7576
Page<OrderItemEntity> findOrderItemsByKeyword(@Param("keyword") String keyword, Pageable pageable);
7677

7778
Boolean existsByProductIdAndUserId(UUID productId, UUID userId);
79+
80+
Boolean existsByProduct_IdAndUser_IdAndScriptTrueAndOrder_OrderStatusAndCreatedAtAfter(UUID productId, UUID userId, OrderStatus status, LocalDateTime oneYearAgo);
7881
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package PodoeMarket.podoemarket.common.repository;
2+
3+
import PodoeMarket.podoemarket.common.entity.PdfDownloadLogEntity;
4+
import io.lettuce.core.dynamic.annotation.Param;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
8+
import java.util.List;
9+
import java.util.UUID;
10+
11+
public interface PdfDownloadLogRepository extends JpaRepository<PdfDownloadLogEntity, Long> {
12+
@Query("""
13+
SELECT p.orderItemId
14+
FROM PdfDownloadLogEntity p
15+
WHERE p.orderItemId IN :orderItemIds
16+
AND p.userId = :userId
17+
""")
18+
List<UUID> findDownloadedOrderItemIds(@Param("orderItemIds") List<UUID> orderItemIds, @Param("userId") UUID userId);
19+
20+
Boolean existsByOrderItemIdAndUserId(UUID orderItemId, UUID userId);
21+
}

src/main/java/PodoeMarket/podoemarket/product/controller/ProductController.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,12 @@ public ResponseEntity<StreamingResponseBody> scriptPreview(@RequestParam("script
114114
@GetMapping("/description")
115115
public ResponseEntity<StreamingResponseBody> descriptionView(@RequestParam("script") UUID productId) {
116116
try{
117-
// 데이터베이스 작업 (트랜잭션 내에서 수행)
118-
final ProductEntity product = productService.getProduct(productId);
119-
final String s3Key = product.getDescriptionPath();
120-
121-
if (s3Key == null)
122-
return ResponseEntity.noContent().build();
123-
124-
final String preSignedURL = s3Service.generatePreSignedURL(s3Key);
117+
StreamingResponseBody description = productService.viewDescription(productId);
125118

126-
return productService.generateFullPDF(preSignedURL);
119+
return ResponseEntity.ok()
120+
.header("Content-Disposition", "inline; filename=\"description.pdf\"")
121+
.contentType(MediaType.APPLICATION_PDF)
122+
.body(description);
127123
} catch(Exception e) {
128124
StreamingResponseBody errorBody = outputStream -> {
129125
String errorMsg = "{\"error\": \"" + e.getMessage() + "\"}";
@@ -160,12 +156,12 @@ public ResponseEntity<?> productLike(@AuthenticationPrincipal UserEntity userInf
160156
@GetMapping("/view")
161157
public ResponseEntity<StreamingResponseBody> scriptView(@RequestParam("script") UUID productId) {
162158
try{
163-
// 데이터베이스 작업 (트랜잭션 내에서 수행)
164-
final ProductEntity product = productService.getProduct(productId);
165-
final String s3Key = product.getFilePath();
166-
final String preSignedURL = s3Service.generatePreSignedURL(s3Key);
159+
StreamingResponseBody script = productService.viewScript(productId);
167160

168-
return productService.generateFullPDF(preSignedURL);
161+
return ResponseEntity.ok()
162+
.header("Content-Disposition", "inline; filename=\"script.pdf\"")
163+
.contentType(MediaType.APPLICATION_PDF)
164+
.body(script);
169165
} catch(Exception e) {
170166
StreamingResponseBody errorBody = outputStream -> {
171167
String errorMsg = "{\"error\": \"" + e.getMessage() + "\"}";

src/main/java/PodoeMarket/podoemarket/product/dto/response/ScriptDetailResponseDTO.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package PodoeMarket.podoemarket.product.dto.response;
22

3-
import PodoeMarket.podoemarket.common.entity.type.PlayType;
4-
import PodoeMarket.podoemarket.common.entity.type.ProductStatus;
5-
import PodoeMarket.podoemarket.common.entity.type.StageType;
6-
import PodoeMarket.podoemarket.common.entity.type.StandardType;
3+
import PodoeMarket.podoemarket.common.entity.type.*;
74
import lombok.AllArgsConstructor;
85
import lombok.Builder;
96
import lombok.Data;
@@ -35,7 +32,7 @@ public class ScriptDetailResponseDTO {
3532
// 0 : 아무것도 구매 X
3633
// 1 : 대본 or 대본 + 공연권 (대본 권리 기간 유효 시)
3734
// 2 : 공연권만 보유
38-
private Integer buyStatus;
35+
private List<BuyOption> buyOptions;
3936
private Boolean like;
4037
private Long likeCount;
4138
private Long viewCount;

src/main/java/PodoeMarket/podoemarket/product/service/ProductService.java

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package PodoeMarket.podoemarket.product.service;
22

33
import PodoeMarket.podoemarket.common.entity.*;
4-
import PodoeMarket.podoemarket.common.entity.type.StageType;
5-
import PodoeMarket.podoemarket.common.entity.type.StandardType;
4+
import PodoeMarket.podoemarket.common.entity.type.*;
65
import PodoeMarket.podoemarket.common.repository.*;
7-
import PodoeMarket.podoemarket.common.entity.type.PlayType;
8-
import PodoeMarket.podoemarket.common.entity.type.ProductStatus;
96
import PodoeMarket.podoemarket.product.dto.request.ReviewRequestDTO;
107
import PodoeMarket.podoemarket.product.dto.request.ReviewUpdateRequestDTO;
118
import PodoeMarket.podoemarket.product.dto.response.ReviewResponseDTO;
@@ -15,8 +12,11 @@
1512
import PodoeMarket.podoemarket.product.type.ReviewSortType;
1613
import PodoeMarket.podoemarket.service.S3Service;
1714
import PodoeMarket.podoemarket.service.ViewCountService;
15+
import com.amazonaws.services.s3.AmazonS3;
16+
import com.amazonaws.services.s3.model.S3Object;
1817
import com.itextpdf.io.source.ByteArrayOutputStream;
1918
import org.apache.pdfbox.Loader;
19+
import org.springframework.beans.factory.annotation.Value;
2020
import org.springframework.http.MediaType;
2121
import org.springframework.http.ResponseEntity;
2222
import org.springframework.transaction.annotation.Transactional;
@@ -37,6 +37,7 @@
3737
import java.io.OutputStream;
3838
import java.net.URL;
3939
import java.time.LocalDateTime;
40+
import java.util.ArrayList;
4041
import java.util.List;
4142
import java.util.UUID;
4243
import java.util.zip.ZipEntry;
@@ -48,13 +49,16 @@
4849
public class ProductService {
4950
private final ProductRepository productRepo;
5051
private final OrderItemRepository orderItemRepo;
51-
private final ApplicantRepository applicantRepo;
5252
private final ProductLikeRepository productLikeRepo;
5353
private final ReviewRepository reviewRepo;
5454
private final ReviewLikeRepository reviewLikeRepo;
5555

5656
private final ViewCountService viewCountService;
5757
private final S3Service s3Service;
58+
private final AmazonS3 amazonS3;
59+
60+
@Value("${cloud.aws.s3.bucket}")
61+
private String bucket;
5862

5963
public List<ScriptListResponseDTO.ProductListDTO> getPlayList(int page, UserEntity userInfo, PlayType playType, int pageSize, ProductSortType sortType) {
6064
try {
@@ -128,7 +132,7 @@ public ScriptDetailResponseDTO getScriptDetailInfo(UserEntity userInfo, UUID pro
128132
.scene(script.getScene())
129133
.act(script.getAct())
130134
.intention(script.getIntention())
131-
.buyStatus(buyStatus(userInfo, productId)) // 로그인한 유저의 해당 작품 구매 이력 확인
135+
.buyOptions(buyOption(userInfo, productId)) // 로그인한 유저의 해당 작품 구매 이력 확인
132136
.like(getProductLikeStatus(userInfo, productId)) // 로그인한 유저의 좋아요 여부 확인
133137
.likeCount(script.getLikeCount()) // 총 좋아요 수
134138
.isReviewWritten(isReviewWritten)
@@ -193,19 +197,28 @@ public String toggleLikeProduct(UserEntity userInfo, UUID productId) {
193197
}
194198
}
195199

196-
public ResponseEntity<StreamingResponseBody> generateFullPDF(String preSignedURL) {
197-
StreamingResponseBody stream = outputStream -> {
198-
try {
199-
streamPdfFromZip(preSignedURL, outputStream);
200-
} catch (Exception e) {
201-
throw new RuntimeException("ZIP에서 PDF 추출 또는 스트리밍 중 오류 발생", e);
200+
public StreamingResponseBody viewScript(final UUID productId) {
201+
return outputStream -> {
202+
ProductEntity product = getProduct(productId);
203+
204+
try(S3Object s3Object = amazonS3.getObject(bucket, product.getFilePath());
205+
InputStream s3Stream = s3Object.getObjectContent()) {
206+
207+
streamPdfFromZip(s3Stream, outputStream);
202208
}
203209
};
210+
}
204211

205-
return ResponseEntity.ok()
206-
.header("Content-Disposition", "inline; filename=\"script.pdf\"")
207-
.contentType(MediaType.APPLICATION_PDF)
208-
.body(stream);
212+
public StreamingResponseBody viewDescription(final UUID productId) {
213+
return outputStream -> {
214+
ProductEntity product = getProduct(productId);
215+
216+
try(S3Object s3Object = amazonS3.getObject(bucket, product.getDescriptionPath());
217+
InputStream s3Stream = s3Object.getObjectContent()) {
218+
219+
streamPdfFromZip(s3Stream, outputStream);
220+
}
221+
};
209222
}
210223

211224
public boolean getProductLikeStatus(final UserEntity userInfo, final UUID productId) {
@@ -405,17 +418,18 @@ private static byte[] extractPdfFromZip(String preSignedURL) throws IOException
405418
}
406419
}
407420

408-
// PDF 추출 메서드 (스트리밍 방식)
409-
private static void streamPdfFromZip(String preSignedURL, OutputStream outputStream) throws IOException {
410-
try (InputStream inputStream = new URL(preSignedURL).openStream();
411-
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
421+
// ZIP → PDF 추출 (스트리밍)
422+
private static void streamPdfFromZip(InputStream zipStream, OutputStream outputStream) throws IOException {
423+
try (ZipInputStream zipInputStream = new ZipInputStream(zipStream)) {
412424
ZipEntry entry;
413425
boolean found = false;
414426

427+
byte[] buffer = new byte[8192];
428+
415429
while ((entry = zipInputStream.getNextEntry()) != null) {
430+
416431
if (entry.getName().toLowerCase().endsWith(".pdf")) {
417432
found = true;
418-
byte[] buffer = new byte[8192]; // 8KB 씩 스트리밍
419433
int len;
420434

421435
while ((len = zipInputStream.read(buffer)) > 0) {
@@ -474,31 +488,36 @@ protected void createProductLike(final ProductLikeEntity like, final UUID produc
474488
}
475489
}
476490

477-
private int buyStatus(final UserEntity userInfo, final UUID productId) {
491+
private List<BuyOption> buyOption(final UserEntity userInfo, final UUID productId) {
478492
try {
479-
if(userInfo == null)
480-
return 0;
493+
// <대본>
494+
// 권리기간(열람기간) : 3개월
495+
// 환불 : 불가
496+
// 한 번에 1개만 소유 가능
481497

482-
final List<OrderItemEntity> orderItems = orderItemRepo.findByProductIdAndUserId(productId, userInfo.getId());
498+
List<BuyOption> options = new ArrayList<>();
483499

484-
for(OrderItemEntity item : orderItems) {
485-
final boolean isBuyScript = item.getScript(); // 대본 구매 여부
486-
final boolean isExpiryDate = LocalDateTime.now().isAfter(item.getCreatedAt().plusYears(1)); // 권리 기간 만료 여부
487-
final boolean isBuyPerformance = applicantRepo.existsByOrderItemId(item.getId()); // 공연권 구매 여부
500+
if (userInfo == null) {
501+
options.add(BuyOption.SCRIPT);
502+
options.add(BuyOption.PERFORMANCE);
488503

489-
if(isBuyScript && !isExpiryDate) { // 대본 구매 (대본 권리 기간 유효)
490-
return 1;
491-
} else if(isBuyScript && !isExpiryDate && isBuyPerformance) { // 대본 + 공연권 구매 (대본 권리 기간 유효)
492-
return 1;
493-
}
494-
else if(isBuyScript && isExpiryDate && isBuyPerformance) { // 공연권만 보유
495-
return 2;
496-
}
504+
return options;
497505
}
498506

499-
return 0;
507+
boolean hasValidScript = orderItemRepo.existsByProduct_IdAndUser_IdAndScriptTrueAndOrder_OrderStatusAndCreatedAtAfter(
508+
productId, userInfo.getId(), OrderStatus.PAID, LocalDateTime.now().minusMonths(3)
509+
);
510+
511+
// 유효한 대본이 없으면 대본 구매 가능
512+
if(!hasValidScript)
513+
options.add(BuyOption.SCRIPT);
514+
515+
// 공연권은 항상 가능
516+
options.add(BuyOption.PERFORMANCE);
517+
518+
return options;
500519
} catch (Exception e) {
501-
return 0; // 오류 발생 시 구매하지 않은 것으로 처리
520+
throw e;
502521
}
503522
}
504523

src/main/java/PodoeMarket/podoemarket/profile/controller/MypageController.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,14 @@ public ResponseEntity<?> apply(@RequestBody ApplyRequestDTO dto) {
195195
}
196196

197197
@GetMapping(value = "/download", produces = "application/json; charset=UTF-8")
198-
public ResponseEntity<?> scriptDownload(@AuthenticationPrincipal UserEntity userInfo, @RequestParam("id") UUID orderId) {
198+
public ResponseEntity<?> scriptDownload(@AuthenticationPrincipal UserEntity userInfo, @RequestParam("id") UUID orderItemId) {
199199
try {
200-
ScriptInfoResponseDTO scriptInfo = mypageService.checkValidation(orderId);
201-
202-
byte[] fileData = mypageService.downloadFile(scriptInfo.getFilePath(), userInfo.getEmail());
203-
204-
String encodedFilename = URLEncoder.encode(scriptInfo.getTitle(), StandardCharsets.UTF_8);
200+
ScriptDownloadResponseDTO resDTO = mypageService.downloadFile(orderItemId, userInfo);
205201

206202
return ResponseEntity.ok()
207203
.contentType(MediaType.APPLICATION_PDF) // PDF 파일 형식으로 설정
208-
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
209-
.body(fileData);
204+
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + resDTO.getFileName())
205+
.body(resDTO.getFileData());
210206
} catch (Exception e) {
211207
ResponseDTO resDTO = ResponseDTO.builder().error(e.getMessage()).build();
212208
return ResponseEntity.badRequest().body(resDTO);

src/main/java/PodoeMarket/podoemarket/profile/dto/response/OrderPerformanceResponseDTO.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package PodoeMarket.podoemarket.profile.dto.response;
22

3+
import PodoeMarket.podoemarket.common.entity.type.OrderStatus;
34
import PodoeMarket.podoemarket.common.entity.type.PlayType;
45
import PodoeMarket.podoemarket.common.entity.type.ProductStatus;
56
import lombok.AllArgsConstructor;
@@ -39,10 +40,10 @@ public static class OrderPerformanceDTO {
3940
private String imagePath;
4041
private ProductStatus checked;
4142
private PlayType playType;
42-
private Integer performanceAmount;
4343
private Long performancePrice;
4444
private Long performanceTotalPrice;
4545
private Integer possibleCount;
46+
private Boolean isDownloaded;
4647

4748
private UUID productId;
4849
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package PodoeMarket.podoemarket.profile.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
@Data
9+
@Builder
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
public class ScriptDownloadResponseDTO {
13+
private String fileName;
14+
private byte[] fileData;
15+
}

0 commit comments

Comments
 (0)