Skip to content

Commit 4c2c5c6

Browse files
Merge pull request #160 from prgrms-web-devcourse-final-project/feature/EA3-161-study-image
[EA3-161] feature: 스터디 생성, 수정 시 이미지 기능 구현 및 테스트
2 parents 52ffbec + ecca1b8 commit 4c2c5c6

File tree

9 files changed

+159
-68
lines changed

9 files changed

+159
-68
lines changed

src/main/java/grep/neogul_coder/domain/study/controller/StudyController.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import lombok.RequiredArgsConstructor;
1111
import org.springframework.data.domain.Pageable;
1212
import org.springframework.data.web.PageableDefault;
13+
import org.springframework.http.MediaType;
1314
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1415
import org.springframework.web.bind.annotation.*;
16+
import org.springframework.web.multipart.MultipartFile;
1517

18+
import java.io.IOException;
1619
import java.util.List;
1720

1821
@RequestMapping("/api/studies")
@@ -65,18 +68,20 @@ public ApiResponse<StudyMemberInfoResponse> getMyStudyMemberInfo(@PathVariable("
6568
return ApiResponse.success(studyService.getMyStudyMemberInfo(studyId, userId));
6669
}
6770

68-
@PostMapping
69-
public ApiResponse<Long> createStudy(@RequestBody @Valid StudyCreateRequest request,
70-
@AuthenticationPrincipal Principal userDetails) {
71-
Long id = studyService.createStudy(request, userDetails.getUserId());
71+
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
72+
public ApiResponse<Long> createStudy(@RequestPart("request") @Valid StudyCreateRequest request,
73+
@RequestPart(value = "image", required = false) MultipartFile image,
74+
@AuthenticationPrincipal Principal userDetails) throws IOException {
75+
Long id = studyService.createStudy(request, userDetails.getUserId(), image);
7276
return ApiResponse.success(id);
7377
}
7478

7579
@PutMapping("/{studyId}")
7680
public ApiResponse<Void> updateStudy(@PathVariable("studyId") Long studyId,
77-
@RequestBody @Valid StudyUpdateRequest request,
78-
@AuthenticationPrincipal Principal userDetails) {
79-
studyService.updateStudy(studyId, request, userDetails.getUserId());
81+
@RequestPart @Valid StudyUpdateRequest request,
82+
@RequestPart(value = "image", required = false) MultipartFile image,
83+
@AuthenticationPrincipal Principal userDetails) throws IOException {
84+
studyService.updateStudy(studyId, request, userDetails.getUserId(), image);
8085
return ApiResponse.noContent();
8186
}
8287

src/main/java/grep/neogul_coder/domain/study/controller/StudySpecification.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import io.swagger.v3.oas.annotations.Operation;
99
import io.swagger.v3.oas.annotations.tags.Tag;
1010
import org.springframework.data.domain.Pageable;
11+
import org.springframework.web.multipart.MultipartFile;
1112

13+
import java.io.IOException;
1214
import java.util.List;
1315

1416
@Tag(name = "Study", description = "스터디 API")
@@ -36,10 +38,10 @@ public interface StudySpecification {
3638
ApiResponse<StudyMemberInfoResponse> getMyStudyMemberInfo(Long studyId, Principal userDetails);
3739

3840
@Operation(summary = "스터디 생성", description = "새로운 스터디를 생성합니다.")
39-
ApiResponse<Long> createStudy(StudyCreateRequest request, Principal userDetails);
41+
ApiResponse<Long> createStudy(StudyCreateRequest request, MultipartFile image, Principal userDetails) throws IOException;
4042

4143
@Operation(summary = "스터디 수정", description = "스터디를 수정합니다.")
42-
ApiResponse<Void> updateStudy(Long studyId, StudyUpdateRequest request, Principal userDetails);
44+
ApiResponse<Void> updateStudy(Long studyId, StudyUpdateRequest request, MultipartFile image, Principal userDetails) throws IOException;
4345

4446
@Operation(summary = "스터디 삭제", description = "스터디를 삭제합니다.")
4547
ApiResponse<Void> deleteStudy(Long studyId, Principal userDetails);

src/main/java/grep/neogul_coder/domain/study/controller/dto/request/StudyCreateRequest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public class StudyCreateRequest {
4545
@Schema(description = "스터디 소개", example = "자바 스터디입니다.")
4646
private String introduction;
4747

48-
@NotBlank
4948
@Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg")
5049
private String imageUrl;
5150

@@ -65,7 +64,7 @@ private StudyCreateRequest(String name, Category category, int capacity, StudyTy
6564
this.imageUrl = imageUrl;
6665
}
6766

68-
public Study toEntity() {
67+
public Study toEntity(String imageUrl) {
6968
return Study.builder()
7069
.name(this.name)
7170
.category(this.category)
@@ -75,7 +74,7 @@ public Study toEntity() {
7574
.startDate(this.startDate)
7675
.endDate(this.endDate)
7776
.introduction(this.introduction)
78-
.imageUrl(this.imageUrl)
77+
.imageUrl(imageUrl)
7978
.build();
8079
}
8180
}

src/main/java/grep/neogul_coder/domain/study/controller/dto/request/StudyUpdateRequest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public class StudyUpdateRequest {
3838
@Schema(description = "스터디 소개", example = "자바 스터디입니다.")
3939
private String introduction;
4040

41-
@NotBlank
4241
@Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg")
4342
private String imageUrl;
4443

@@ -57,7 +56,7 @@ private StudyUpdateRequest(String name, Category category, int capacity, StudyTy
5756
this.imageUrl = imageUrl;
5857
}
5958

60-
public Study toEntity() {
59+
public Study toEntity(String imageUrl) {
6160
return Study.builder()
6261
.name(this.name)
6362
.category(this.category)
@@ -66,7 +65,7 @@ public Study toEntity() {
6665
.location(this.location)
6766
.startDate(this.startDate)
6867
.introduction(this.introduction)
69-
.imageUrl(this.imageUrl)
68+
.imageUrl(imageUrl)
7069
.build();
7170
}
7271
}

src/main/java/grep/neogul_coder/domain/study/service/StudyService.java

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@
1717
import grep.neogul_coder.domain.users.repository.UserRepository;
1818
import grep.neogul_coder.global.exception.business.BusinessException;
1919
import grep.neogul_coder.global.exception.business.NotFoundException;
20+
import grep.neogul_coder.global.utils.upload.FileUploadResponse;
21+
import grep.neogul_coder.global.utils.upload.FileUsageType;
22+
import grep.neogul_coder.global.utils.upload.uploader.GcpFileUploader;
23+
import grep.neogul_coder.global.utils.upload.uploader.LocalFileUploader;
2024
import lombok.RequiredArgsConstructor;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.core.env.Environment;
2127
import org.springframework.data.domain.Page;
2228
import org.springframework.data.domain.Pageable;
2329
import org.springframework.stereotype.Service;
2430
import org.springframework.transaction.annotation.Transactional;
31+
import org.springframework.web.multipart.MultipartFile;
2532

33+
import java.io.IOException;
2634
import java.util.List;
2735
import java.util.Optional;
2836

@@ -43,6 +51,15 @@ public class StudyService {
4351
private final UserRepository userRepository;
4452
private final BuddyEnergyService buddyEnergyService;
4553

54+
@Autowired(required = false)
55+
private GcpFileUploader gcpFileUploader;
56+
57+
@Autowired(required = false)
58+
private LocalFileUploader localFileUploader;
59+
60+
@Autowired
61+
private Environment environment;
62+
4663
public StudyItemPagingResponse getMyStudiesPaging(Pageable pageable, Long userId) {
4764
Page<StudyItemResponse> page = studyQueryRepository.findMyStudiesPaging(pageable, userId);
4865
return StudyItemPagingResponse.of(page);
@@ -90,10 +107,12 @@ public StudyMemberInfoResponse getMyStudyMemberInfo(Long studyId, Long userId) {
90107
}
91108

92109
@Transactional
93-
public Long createStudy(StudyCreateRequest request, Long userId) {
110+
public Long createStudy(StudyCreateRequest request, Long userId, MultipartFile image) throws IOException {
94111
validateLocation(request.getStudyType(), request.getLocation());
95112

96-
Study study = studyRepository.save(request.toEntity());
113+
String imageUrl = createImageUrl(userId, image);
114+
115+
Study study = studyRepository.save(request.toEntity(imageUrl));
97116

98117
StudyMember leader = StudyMember.builder()
99118
.study(study)
@@ -106,7 +125,7 @@ public Long createStudy(StudyCreateRequest request, Long userId) {
106125
}
107126

108127
@Transactional
109-
public void updateStudy(Long studyId, StudyUpdateRequest request, Long userId) {
128+
public void updateStudy(Long studyId, StudyUpdateRequest request, Long userId, MultipartFile image) throws IOException {
110129
Study study = studyRepository.findById(studyId)
111130
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
112131

@@ -115,6 +134,8 @@ public void updateStudy(Long studyId, StudyUpdateRequest request, Long userId) {
115134
validateStudyLeader(studyId, userId);
116135
validateStudyStartDate(request, study);
117136

137+
String imageUrl = updateImageUrl(userId, image, study.getImageUrl());
138+
118139
study.update(
119140
request.getName(),
120141
request.getCategory(),
@@ -123,7 +144,7 @@ public void updateStudy(Long studyId, StudyUpdateRequest request, Long userId) {
123144
request.getLocation(),
124145
request.getStartDate(),
125146
request.getIntroduction(),
126-
request.getImageUrl()
147+
imageUrl
127148
);
128149
}
129150

@@ -181,4 +202,39 @@ private void validateStudyDeletable(Long studyId) {
181202
throw new BusinessException(STUDY_DELETE_NOT_ALLOWED);
182203
}
183204
}
205+
206+
private boolean isProductionEnvironment() {
207+
for (String profile : environment.getActiveProfiles()) {
208+
if ("prod".equals(profile)) {
209+
return true;
210+
}
211+
}
212+
return false;
213+
}
214+
215+
private String createImageUrl(Long userId, MultipartFile image) throws IOException {
216+
String imageUrl = null;
217+
if (isImgExists(image)) {
218+
FileUploadResponse uploadResult = isProductionEnvironment()
219+
? gcpFileUploader.upload(image, userId, FileUsageType.STUDY_COVER, userId)
220+
: localFileUploader.upload(image, userId, FileUsageType.STUDY_COVER, userId);
221+
imageUrl = uploadResult.getFileUrl();
222+
}
223+
return imageUrl;
224+
}
225+
226+
private String updateImageUrl(Long userId, MultipartFile image, String originalImageUrl) throws IOException {
227+
if (isImgExists(image)) {
228+
FileUploadResponse uploadResult = isProductionEnvironment()
229+
? gcpFileUploader.upload(image, userId, FileUsageType.STUDY_COVER, userId)
230+
: localFileUploader.upload(image, userId, FileUsageType.STUDY_COVER, userId);
231+
return uploadResult.getFileUrl();
232+
}
233+
return originalImageUrl;
234+
}
235+
236+
237+
private boolean isImgExists(MultipartFile image) {
238+
return image != null && !image.isEmpty();
239+
}
184240
}

src/main/java/grep/neogul_coder/domain/users/service/UserService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void updateProfile(Long userId, String nickname, MultipartFile profileIma
100100
FileUploadResponse response = isProductionEnvironment()
101101
? gcpFileUploader.upload(profileImage, userId, FileUsageType.PROFILE, userId)
102102
: localFileUploader.upload(profileImage, userId, FileUsageType.PROFILE, userId);
103-
uploadedImageUrl = response.fileUrl();
103+
uploadedImageUrl = response.getFileUrl();
104104
} else {
105105
uploadedImageUrl = user.getProfileImageUrl();
106106
}

src/main/java/grep/neogul_coder/global/utils/upload/AbstractFileManager.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ public FileUploadResponse upload(MultipartFile file, Long uploaderId, FileUsageT
4848

4949
uploadFile(file, buildFullPath(savePath, renameFileName)); // 실제 파일 업로드(구현체에서 구현)
5050

51-
return new FileUploadResponse(
52-
originFileName,
53-
renameFileName,
54-
usageType,
55-
fileUrl,
56-
savePath,
57-
uploaderId,
58-
usageRefId
59-
);
51+
return FileUploadResponse.builder()
52+
.originFileName(originFileName)
53+
.renameFileName(renameFileName)
54+
.usageType(usageType)
55+
.savePath(savePath)
56+
.fileUrl(fileUrl)
57+
.uploaderId(uploaderId)
58+
.usageRefId(usageRefId)
59+
.build();
6060
}
6161

6262
// 실제 파일 업로드를 수행
Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
package grep.neogul_coder.global.utils.upload;
22

3-
public record FileUploadResponse(
4-
String originFileName, // 원본 파일명
5-
String renameFileName, // UUID로 변경된 파일명
6-
FileUsageType usageType, // 파일 사용 목적
7-
String savePath, // 저장 경로
8-
String fileUrl, // 전체 URL
9-
Long uploaderId, // 업로더 ID
10-
Long usageRefId // 파일이 참조되는 도메인 ID
11-
) {
3+
import lombok.Builder;
4+
import lombok.Getter;
125

6+
@Getter
7+
public class FileUploadResponse {
8+
private String originFileName; // 원본 파일명
9+
private String renameFileName; // UUID로 변경된 파일명
10+
private FileUsageType usageType; // 파일 사용 목적
11+
private String savePath; // 저장 경로
12+
private String fileUrl; // 전체 URL
13+
private Long uploaderId; // 업로더 ID
14+
private Long usageRefId; // 파일이 참조되는 도메인 ID
15+
16+
@Builder
17+
public FileUploadResponse(String originFileName, String renameFileName, FileUsageType usageType, String savePath, String fileUrl, Long uploaderId, Long usageRefId) {
18+
this.originFileName = originFileName;
19+
this.renameFileName = renameFileName;
20+
this.usageType = usageType;
21+
this.savePath = savePath;
22+
this.fileUrl = fileUrl;
23+
this.uploaderId = uploaderId;
24+
this.usageRefId = usageRefId;
25+
}
1326
}

0 commit comments

Comments
 (0)