Skip to content

Commit 15faf57

Browse files
authored
[feat] 펀딩 이미지 구현 (#350)
* [feat] 펀딩 이미지 구현 * [feat] work * work
1 parent d80ec5b commit 15faf57

File tree

9 files changed

+222
-44
lines changed

9 files changed

+222
-44
lines changed

src/main/java/com/back/domain/funding/controller/FundingController.java

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
import com.back.domain.funding.dto.response.FundingCreateResponse;
77
import com.back.domain.funding.dto.response.FundingDetailResponse;
88
import com.back.domain.funding.entity.Funding;
9+
import com.back.domain.funding.entity.FundingImage;
910
import com.back.domain.funding.entity.FundingStatus;
1011
import com.back.domain.funding.service.FundingService;
1112
import com.back.global.rsData.RsData;
13+
import com.back.global.s3.FileType;
14+
import com.back.global.s3.S3Service;
15+
import com.back.global.s3.UploadResultResponse;
1216
import io.swagger.v3.oas.annotations.Operation;
1317
import io.swagger.v3.oas.annotations.Parameter;
1418
import io.swagger.v3.oas.annotations.media.Content;
@@ -23,7 +27,9 @@
2327
import org.springframework.security.access.prepost.PreAuthorize;
2428
import org.springframework.security.core.annotation.AuthenticationPrincipal;
2529
import org.springframework.web.bind.annotation.*;
30+
import org.springframework.web.multipart.MultipartFile;
2631

32+
import java.util.List;
2733
import java.util.Set;
2834

2935
@RestController
@@ -33,6 +39,7 @@
3339
public class FundingController {
3440

3541
private final FundingService fundingService;
42+
private final S3Service s3Service;
3643

3744
@PostMapping
3845
@PreAuthorize("hasAuthority('ROLE_ARTIST') or hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
@@ -48,12 +55,19 @@ public class FundingController {
4855
"title": "한정판 키링 펀딩",
4956
"description": "한정판 키링입니다.",
5057
"categoryId": 1,
51-
"imageUrl": "https://testImage.jpg",
5258
"targetAmount": 500000,
5359
"price": 30000,
5460
"stock": 200,
5561
"startDate": "2025-11-01 00:00:00",
56-
"endDate": "2025-12-15 23:59:59"
62+
"endDate": "2025-12-15 23:59:59",
63+
"images": [
64+
{
65+
"url": "https://test.jpg",
66+
"type": "MAIN",
67+
"s3Key": "funding-images/test.jpg",
68+
"originalFileName": "test.JPG"
69+
}
70+
]
5771
}
5872
"""
5973
)
@@ -72,6 +86,60 @@ public class FundingController {
7286
.body(new RsData<>("201", "펀딩이 생성되었습니다.", response));
7387
}
7488

89+
@PostMapping("/images")
90+
@PreAuthorize("hasAuthority('ROLE_ARTIST') or hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
91+
@Operation(
92+
summary = "펀딩 이미지 업로드",
93+
description = "펀딩에 사용될 이미지를 업로드합니다. " +
94+
"files -> 업로드할 파일 리스트. 이미지(jpg, png 등), 문서(pdf, doc 등)<br>" +
95+
"types -> 업로드할 파일 타입. 대표 이미지(MAIN), 추가 이미지(ADDITIONAL), 썸네일(THUMBNAIL), 문서(DOCUMENT)"
96+
) public ResponseEntity<RsData<List<UploadResultResponse>>> uploadFundingImages(
97+
@RequestPart List<MultipartFile> files,
98+
@Parameter(hidden = true)
99+
@RequestParam List<FileType> types) {
100+
List<UploadResultResponse> uploaded = s3Service.uploadFiles(files, "funding-images", types);
101+
return ResponseEntity.ok(RsData.of("200", "이미지 업로드 성공", uploaded));
102+
}
103+
104+
/** 펀딩 이미지 개별 삭제 (S3) */
105+
@DeleteMapping("/images")
106+
@PreAuthorize("hasAuthority('ROLE_ARTIST') or hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
107+
@Operation(
108+
summary = "S3 펀딩 이미지 개별 삭제",
109+
description = "s3Key를 사용하여 S3에 업로드된 펀딩 이미지를 삭제합니다. " +
110+
"펀딩 등록/수정 중 사용자가 업로드한 이미지를 다시 삭제할 때 사용됩니다."
111+
)
112+
public ResponseEntity<RsData<String>> deleteFundingImage(
113+
@Parameter(description = "삭제할 파일의 s3Key", required = true)
114+
@RequestParam String s3Key) {
115+
116+
s3Service.deleteFile(s3Key);
117+
return ResponseEntity.ok(RsData.of("200", "파일이 성공적으로 삭제되었습니다.", s3Key));
118+
}
119+
120+
/** 펀딩 문서 다운로드 */
121+
@GetMapping("/images/download/{id}")
122+
@Operation(
123+
summary = "펀딩 문서 다운로드",
124+
description = "DOCUMENT 타입의 문서 파일 다운로드.<br>" +
125+
"브라우저는 Content-Disposition 헤더를 보고 파일 다운로드 처리합니다."
126+
)
127+
public ResponseEntity<byte[]> downloadFundingDocument(
128+
@Parameter(description = "펀딩 ID", required = true)
129+
@PathVariable @Positive Long id) {
130+
131+
// 펀딩 ID로 DOCUMENT 타입 이미지 조회
132+
FundingImage document = fundingService.getFundingDocument(id);
133+
// S3에서 파일 다운로드
134+
byte[] fileBytes = s3Service.downloadFile(document.getS3Key());
135+
136+
// 원본 파일명으로 응답
137+
return ResponseEntity.ok()
138+
.header("Content-Disposition", "attachment; filename=\"" + document.getOriginalFilename() + "\"")
139+
.header("Content-Type", "application/octet-stream")
140+
.body(fileBytes);
141+
}
142+
75143
@GetMapping("/{id}")
76144
@Operation(summary = "펀딩 상세 조회")
77145
public ResponseEntity<RsData<FundingDetailResponse>> getFunding(@PathVariable @Positive Long id) {

src/main/java/com/back/domain/funding/dto/request/FundingCreateRequest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.back.domain.funding.dto.request;
22

33

4+
import com.back.global.s3.S3FileRequest;
45
import com.fasterxml.jackson.annotation.JsonFormat;
56
import jakarta.validation.constraints.NotBlank;
67
import jakarta.validation.constraints.NotNull;
78
import jakarta.validation.constraints.Positive;
89
import jakarta.validation.constraints.Size;
910

1011
import java.time.LocalDateTime;
12+
import java.util.List;
1113

1214
public record FundingCreateRequest(
1315
@NotBlank(message = "펀딩 제목은 필수입니다.")
@@ -20,7 +22,6 @@ public record FundingCreateRequest(
2022
@NotNull(message = "카테고리는 필수입니다.")
2123
Long categoryId, // 상위 카테고리만 가능 (parent == null)
2224

23-
@NotBlank(message = "대표 이미지 URL은 필수입니다.")
2425
String imageUrl,
2526

2627
@NotNull(message = "목표 금액은 필수입니다.")
@@ -39,5 +40,7 @@ public record FundingCreateRequest(
3940

4041
@NotNull(message = "종료일은 필수입니다.")
4142
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
42-
LocalDateTime endDate
43+
LocalDateTime endDate,
44+
45+
List<S3FileRequest> images
4346
) {}

src/main/java/com/back/domain/funding/dto/request/FundingUpdateRequest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.back.domain.funding.dto.request;
22

3+
import com.back.global.s3.S3FileRequest;
34
import io.swagger.v3.oas.annotations.media.Schema;
45
import jakarta.validation.constraints.Future;
56
import jakarta.validation.constraints.Positive;
67
import jakarta.validation.constraints.Size;
78

89
import java.time.LocalDateTime;
10+
import java.util.List;
911

1012
@Schema(description = "펀딩 수정 요청")
1113
public record FundingUpdateRequest(
@@ -33,5 +35,7 @@ public record FundingUpdateRequest(
3335

3436
@Schema(description = "펀딩 종료일", example = "2025-12-31T12:30:00")
3537
@Future
36-
LocalDateTime endDate
38+
LocalDateTime endDate,
39+
40+
List<S3FileRequest> images
3741
) {}

src/main/java/com/back/domain/funding/dto/response/FundingCreateResponse.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import com.fasterxml.jackson.annotation.JsonFormat;
55

66
import java.time.LocalDateTime;
7+
import java.util.List;
8+
import java.util.stream.Collectors;
79

810
public record FundingCreateResponse(
911
Long fundingId,
1012
String title,
1113
String description,
1214
String categoryName,
13-
String imageUrl,
15+
List<FundingDetailResponse.FundingImageResponse> images,
1416
long targetAmount,
1517
long price,
1618
Integer stock,
@@ -25,8 +27,9 @@ public static FundingCreateResponse from(Funding funding) {
2527
funding.getTitle(),
2628
funding.getDescription(),
2729
funding.getCategory().getCategoryName(),
28-
funding.getImageUrl(),
29-
funding.getTargetAmount(),
30+
funding.getImages().stream()
31+
.map(FundingDetailResponse.FundingImageResponse::fromEntity)
32+
.collect(Collectors.toList()), funding.getTargetAmount(),
3033
funding.getPrice(),
3134
funding.getStock(),
3235
funding.getStartDate(),

src/main/java/com/back/domain/funding/dto/response/FundingDetailResponse.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public record FundingDetailResponse(
1212
Long id, // 펀딩 ID
1313
String title, // 펀딩 제목
1414
String description, // 펀딩 설명
15-
String imageUrl, // 펀딩 이미지 URL
15+
List<FundingDetailResponse.FundingImageResponse> images,
1616
String categoryName, // 카테고리 이름
1717
long targetAmount, // 목표 금액
1818
long price, // 가격
@@ -42,7 +42,9 @@ public FundingDetailResponse(Funding funding,
4242
funding.getId(),
4343
funding.getTitle(),
4444
funding.getDescription(),
45-
funding.getImageUrl(),
45+
funding.getImages().stream()
46+
.map(FundingImageResponse::fromEntity)
47+
.collect(Collectors.toList()),
4648
funding.getCategory().getCategoryName(),
4749
funding.getTargetAmount(),
4850
funding.getPrice(),
@@ -61,6 +63,18 @@ public FundingDetailResponse(Funding funding,
6163
);
6264
}
6365

66+
public record FundingImageResponse(
67+
String fileUrl,
68+
String fileType
69+
) {
70+
public static FundingDetailResponse.FundingImageResponse fromEntity(FundingImage img) {
71+
return new FundingDetailResponse.FundingImageResponse(
72+
img.getFileUrl(),
73+
img.getFileType().name()
74+
);
75+
}
76+
}
77+
6478
// 작성자 DTO
6579
public record AuthorDto(Long id, String name, String profileImageUrl, String artistDescription) {
6680
public static AuthorDto from(User user, String artistDescription) {

src/main/java/com/back/domain/funding/entity/Funding.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import java.time.LocalDateTime;
1212
import java.util.ArrayList;
13+
import java.util.Collection;
1314
import java.util.Collections;
1415
import java.util.List;
1516

@@ -93,7 +94,19 @@ public class Funding extends BaseEntity {
9394

9495
@OneToMany(mappedBy = "funding", cascade = CascadeType.ALL, orphanRemoval = true)// 펀딩이 저장/삭제되면 모든 펀딩이미지도 저장/삭제됨, 펀딩에서 이미지를 제거하면 DB에서 해당 이미지도 삭제됨.
9596
@org.hibernate.annotations.BatchSize(size = 100) // N+1 방지: 100개씩 배치로 조회
96-
private List<FundingImage> images = new ArrayList<>();; // 해당 펀딩의 이미지들(대표/추가/썸네일 이미지)
97+
@Builder.Default
98+
private List<FundingImage> images = new ArrayList<>(); // 해당 펀딩의 이미지들(대표/추가/썸네일 이미지)
99+
100+
public void addImage(FundingImage image) {
101+
if (images == null) images = new ArrayList<>();
102+
images.add(image);
103+
image.setFunding(this); // 양방향 연관관계 고정
104+
}
105+
106+
public void addImages(Collection<FundingImage> list) {
107+
if (list == null || list.isEmpty()) return;
108+
list.forEach(this::addImage);
109+
}
97110

98111
// ========== 팩토리 메서드 ==========
99112

@@ -306,7 +319,7 @@ public List<FundingCommunity> getCommunities() {
306319
}
307320

308321
public List<FundingImage> getImages() {
309-
return Collections.unmodifiableList(images);
322+
return Collections.unmodifiableList(images == null ? List.of() : images);
310323
}
311324

312325
// ========== 검증 메서드 ==========
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.back.domain.funding.repository;
2+
3+
import com.back.domain.funding.entity.FundingImage;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface FundingImageRepository extends JpaRepository<FundingImage,Long> {
7+
}

0 commit comments

Comments
 (0)