Skip to content

Commit dcdd0d6

Browse files
authored
Merge pull request #216 from prgrms-web-devcourse-final-project/develop
메인머지 (10/13) - 썸네일 업로드 구현
2 parents d3ecc9e + 6db5898 commit dcdd0d6

File tree

14 files changed

+370
-54
lines changed

14 files changed

+370
-54
lines changed

back/src/main/java/com/back/domain/mentoring/mentoring/controller/MentoringController.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
import com.back.global.rq.Rq;
1111
import com.back.global.rsData.RsData;
1212
import io.swagger.v3.oas.annotations.Operation;
13+
import io.swagger.v3.oas.annotations.Parameter;
14+
import io.swagger.v3.oas.annotations.media.Content;
1315
import io.swagger.v3.oas.annotations.tags.Tag;
1416
import jakarta.validation.Valid;
1517
import lombok.RequiredArgsConstructor;
1618
import org.springframework.data.domain.Page;
19+
import org.springframework.http.MediaType;
1720
import org.springframework.security.access.prepost.PreAuthorize;
1821
import org.springframework.web.bind.annotation.*;
22+
import org.springframework.web.multipart.MultipartFile;
1923

2024
import java.util.List;
2125

@@ -73,14 +77,16 @@ public RsData<MentoringResponse> getMentoring(
7377
);
7478
}
7579

76-
@PostMapping
80+
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
7781
@PreAuthorize("hasRole('MENTOR')")
7882
@Operation(summary = "멘토링 생성", description = "멘토링을 생성합니다. 로그인한 멘토만 생성할 수 있습니다.")
7983
public RsData<MentoringResponse> createMentoring(
80-
@RequestBody @Valid MentoringRequest reqDto
84+
@Parameter(content = @Content(mediaType = "application/json"))
85+
@RequestPart(value = "reqDto") @Valid MentoringRequest reqDto,
86+
@RequestPart(value = "thumb", required = false) MultipartFile thumb
8187
) {
8288
Mentor mentor = memberStorage.findMentorByMember(rq.getActor());
83-
MentoringResponse resDto = mentoringService.createMentoring(reqDto, mentor);
89+
MentoringResponse resDto = mentoringService.createMentoring(reqDto, thumb, mentor);
8490

8591
return new RsData<>(
8692
"201",
@@ -89,14 +95,17 @@ public RsData<MentoringResponse> createMentoring(
8995
);
9096
}
9197

92-
@PutMapping("/{mentoringId}")
98+
@PutMapping(value = "/{mentoringId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
99+
@PreAuthorize("hasRole('MENTOR')")
93100
@Operation(summary = "멘토링 수정", description = "멘토링을 수정합니다. 멘토링 작성자만 접근할 수 있습니다.")
94101
public RsData<MentoringResponse> updateMentoring(
95102
@PathVariable Long mentoringId,
96-
@RequestBody @Valid MentoringRequest reqDto
103+
@Parameter(content = @Content(mediaType = "application/json"))
104+
@RequestPart(value = "reqDto") @Valid MentoringRequest reqDto,
105+
@RequestPart(value = "thumb", required = false) MultipartFile thumb
97106
) {
98107
Mentor mentor = memberStorage.findMentorByMember(rq.getActor());
99-
MentoringResponse resDto = mentoringService.updateMentoring(mentoringId, reqDto, mentor);
108+
MentoringResponse resDto = mentoringService.updateMentoring(mentoringId, reqDto, thumb, mentor);
100109

101110
return new RsData<>(
102111
"200",
@@ -106,6 +115,7 @@ public RsData<MentoringResponse> updateMentoring(
106115
}
107116

108117
@DeleteMapping("/{mentoringId}")
118+
@PreAuthorize("hasRole('MENTOR')")
109119
@Operation(summary = "멘토링 삭제", description = "멘토링을 삭제합니다. 멘토링 작성자만 접근할 수 있습니다.")
110120
public RsData<Void> deleteMentoring(
111121
@PathVariable Long mentoringId

back/src/main/java/com/back/domain/mentoring/mentoring/dto/request/MentoringRequest.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ public record MentoringRequest(
1616

1717
@Schema(description = "멘토링 소개", example = "bio")
1818
@NotNull
19-
String bio,
20-
21-
@Schema(description = "멘토링 썸네일", example = "test.png")
22-
String thumb
19+
String bio
2320
) {
2421
}

back/src/main/java/com/back/domain/mentoring/mentoring/entity/Mentoring.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,15 @@ public class Mentoring extends BaseEntity {
3434
private double rating = 0.0;
3535

3636
@Builder
37-
public Mentoring(Mentor mentor, String title, String bio, String thumb) {
37+
public Mentoring(Mentor mentor, String title, String bio) {
3838
this.mentor = mentor;
3939
this.title = title;
4040
this.bio = bio;
41-
this.thumb = thumb;
4241
}
4342

44-
public void update(String title, String bio, List<Tag> tags, String thumb) {
43+
public void update(String title, String bio, List<Tag> tags) {
4544
this.title = title;
4645
this.bio = bio;
47-
this.thumb = thumb;
4846

4947
updateTags(tags);
5048
}
@@ -59,6 +57,10 @@ public void updateTags(List<Tag> tags) {
5957
}
6058
}
6159

60+
public void updateThumb(String thumb) {
61+
this.thumb = thumb;
62+
}
63+
6264
public void updateRating(double averageRating) {
6365
this.rating = averageRating;
6466
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.back.domain.mentoring.mentoring.error;
2+
3+
import com.back.global.exception.ErrorCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public enum ImageErrorCode implements ErrorCode {
10+
// 400
11+
FILE_SIZE_EXCEEDED("400-1", "이미지 파일 크기는 10MB를 초과할 수 없습니다."),
12+
INVALID_FILE_TYPE("400-2", "이미지 파일만 업로드 가능합니다."),
13+
UNSUPPORTED_IMAGE_FORMAT("400-3", "JPG, PNG 형식만 업로드 가능합니다."),
14+
IMAGE_UPLOAD_FAILED("400-4", "이미지 업로드에 실패했습니다.");
15+
16+
private final String code;
17+
private final String message;
18+
}

back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringService.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse;
99
import com.back.domain.mentoring.mentoring.entity.Mentoring;
1010
import com.back.domain.mentoring.mentoring.entity.Tag;
11+
import com.back.domain.mentoring.mentoring.error.ImageErrorCode;
1112
import com.back.domain.mentoring.mentoring.error.MentoringErrorCode;
1213
import com.back.domain.mentoring.mentoring.repository.MentoringRepository;
1314
import com.back.domain.mentoring.mentoring.repository.TagRepository;
@@ -18,7 +19,9 @@
1819
import org.springframework.data.domain.Pageable;
1920
import org.springframework.stereotype.Service;
2021
import org.springframework.transaction.annotation.Transactional;
22+
import org.springframework.web.multipart.MultipartFile;
2123

24+
import java.io.IOException;
2225
import java.util.ArrayList;
2326
import java.util.List;
2427
import java.util.Set;
@@ -30,6 +33,7 @@ public class MentoringService {
3033
private final MentoringRepository mentoringRepository;
3134
private final MentoringStorage mentoringStorage;
3235
private final TagRepository tagRepository;
36+
private final S3ImageUploader s3ImageUploader;
3337

3438
@Transactional(readOnly = true)
3539
public Page<MentoringWithTagsDto> getMentorings(String keyword, int page, int size) {
@@ -58,20 +62,21 @@ public MentoringResponse getMentoring(Long mentoringId) {
5862
}
5963

6064
@Transactional
61-
public MentoringResponse createMentoring(MentoringRequest reqDto, Mentor mentor) {
65+
public MentoringResponse createMentoring(MentoringRequest reqDto, MultipartFile thumb, Mentor mentor) {
6266
validateMentoringTitle(mentor.getId(), reqDto.title());
6367

6468
Mentoring mentoring = Mentoring.builder()
6569
.mentor(mentor)
6670
.title(reqDto.title())
6771
.bio(reqDto.bio())
68-
.thumb(reqDto.thumb())
6972
.build();
7073

7174
List<Tag> tags = getOrCreateTags(reqDto.tags());
7275
mentoring.updateTags(tags);
7376

74-
mentoringRepository.save(mentoring);
77+
mentoringRepository.saveAndFlush(mentoring);
78+
79+
uploadThumb(thumb, mentoring);
7580

7681
return new MentoringResponse(
7782
MentoringDetailDto.from(mentoring),
@@ -80,15 +85,16 @@ public MentoringResponse createMentoring(MentoringRequest reqDto, Mentor mentor)
8085
}
8186

8287
@Transactional
83-
public MentoringResponse updateMentoring(Long mentoringId, MentoringRequest reqDto, Mentor mentor) {
88+
public MentoringResponse updateMentoring(Long mentoringId, MentoringRequest reqDto, MultipartFile thumb, Mentor mentor) {
8489
Mentoring mentoring = mentoringStorage.findMentoring(mentoringId);
8590

8691
validateOwner(mentoring, mentor);
8792
validateMentoringTitleForUpdate(mentor.getId(), reqDto.title(), mentoringId);
8893

8994
List<Tag> tags = getOrCreateTags(reqDto.tags());
9095

91-
mentoring.update(reqDto.title(), reqDto.bio(), tags, reqDto.thumb());
96+
mentoring.update(reqDto.title(), reqDto.bio(), tags);
97+
uploadThumb(thumb, mentoring);
9298

9399
return new MentoringResponse(
94100
MentoringDetailDto.from(mentoring),
@@ -147,6 +153,23 @@ private List<Tag> createNewTags(List<String> tagNames, Set<String> existingNames
147153
}
148154

149155

156+
// ===== 썸네일 =====
157+
158+
private void uploadThumb(MultipartFile thumb, Mentoring mentoring) {
159+
if (thumb != null && !thumb.isEmpty()) {
160+
String imageUrl = null;
161+
try {
162+
String path = "mentoring/" + mentoring.getId();
163+
imageUrl = s3ImageUploader.upload(thumb, path);
164+
} catch (IOException e) {
165+
throw new ServiceException(ImageErrorCode.IMAGE_UPLOAD_FAILED);
166+
}
167+
168+
mentoring.updateThumb(imageUrl);
169+
}
170+
}
171+
172+
150173
// ===== 유효성 검사 =====
151174

152175
private void validateOwner(Mentoring mentoring, Mentor mentor) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.back.domain.mentoring.mentoring.service;
2+
3+
import com.back.domain.mentoring.mentoring.error.ImageErrorCode;
4+
import com.back.global.exception.ServiceException;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.multipart.MultipartFile;
9+
import software.amazon.awssdk.core.sync.RequestBody;
10+
import software.amazon.awssdk.services.s3.S3Client;
11+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
12+
13+
import java.io.IOException;
14+
import java.util.Set;
15+
16+
@Component
17+
@RequiredArgsConstructor
18+
public class S3ImageUploader {
19+
20+
private final S3Client s3Client;
21+
22+
@Value("${aws.s3.bucket}")
23+
private String bucket;
24+
25+
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
26+
private static final Set<String> ALLOWED_TYPES = Set.of("image/jpeg", "image/jpg", "image/png", "image/webp");
27+
private static final String IMAGE_BASE_PATH = "images/";
28+
29+
public String upload(MultipartFile file, String path) throws IOException {
30+
validateImageFile(file);
31+
32+
String fullPath = IMAGE_BASE_PATH + path;
33+
34+
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
35+
.bucket(bucket)
36+
.key(fullPath)
37+
.contentType(file.getContentType())
38+
.build();
39+
40+
s3Client.putObject(
41+
putObjectRequest,
42+
RequestBody.fromInputStream(file.getInputStream(), file.getSize())
43+
);
44+
45+
return s3Client.utilities()
46+
.getUrl(builder -> builder.bucket(bucket).key(fullPath))
47+
.toString();
48+
}
49+
50+
private void validateImageFile(MultipartFile image) {
51+
if (image.getSize() > MAX_FILE_SIZE) {
52+
throw new ServiceException(ImageErrorCode.FILE_SIZE_EXCEEDED);
53+
}
54+
55+
String contentType = image.getContentType();
56+
if (contentType == null || !contentType.startsWith("image/")) {
57+
throw new ServiceException(ImageErrorCode.INVALID_FILE_TYPE);
58+
}
59+
if (!ALLOWED_TYPES.contains(contentType)) {
60+
throw new ServiceException(ImageErrorCode.INVALID_FILE_TYPE);
61+
}
62+
}
63+
}

back/src/main/java/com/back/domain/post/post/controller/PostController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ public RsData<PostLikedResponse> likePost(@PathVariable Long post_id) {
108108

109109
@Operation(summary = "게시글 싫어요")
110110
@PostMapping("/{post_id}/disliked")
111-
public RsData<PostLikedResponse> disLikePost(@PathVariable Long post_id) {
111+
public RsData<PostDisLikedResponse> disLikePost(@PathVariable Long post_id) {
112112
postLikeService.disLikePost(post_id);
113113

114114
int likeCount = postLikeService.getDisLikeCount(post_id);
115-
PostLikedResponse postLikedResponse = new PostLikedResponse(likeCount);
115+
PostDisLikedResponse postdisLikedResponse = new PostDisLikedResponse(likeCount);
116116

117-
return new RsData<>("200", "게시글 싫어요 성공", postLikedResponse);
117+
return new RsData<>("200", "게시글 싫어요 성공", postdisLikedResponse);
118118
}
119119

120120

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.back.domain.post.post.dto;
2+
3+
public record PostDisLikedResponse(int disLikeCount) {
4+
}

back/src/main/java/com/back/global/initData/SessionInitData.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import com.back.domain.mentoring.slot.service.MentorSlotService;
1717
import lombok.RequiredArgsConstructor;
1818
import org.springframework.boot.CommandLineRunner;
19-
import org.springframework.context.annotation.Bean;
2019
import org.springframework.context.annotation.Configuration;
2120

2221
import java.time.LocalDateTime;
@@ -44,8 +43,8 @@ public CommandLineRunner initData() {
4443
Mentee mentee = menteeRepository.findByMemberIdWithMember(menteeMember.getId()).orElseThrow();
4544

4645
// 멘토링 생성
47-
MentoringRequest mentoringRequest = new MentoringRequest("Test Mentoring", Arrays.asList("Java", "Spring"), "This is a test mentoring.", null);
48-
MentoringResponse mentoringResponse = mentoringService.createMentoring(mentoringRequest, mentor);
46+
MentoringRequest mentoringRequest = new MentoringRequest("Test Mentoring", Arrays.asList("Java", "Spring"), "This is a test mentoring.");
47+
MentoringResponse mentoringResponse = mentoringService.createMentoring(mentoringRequest, null, mentor);
4948

5049
// 멘토 슬롯 생성
5150
MentorSlotRequest mentorSlotRequest = new MentorSlotRequest(mentor.getId(), LocalDateTime.now().plusDays(1), LocalDateTime.now().plusDays(1).plusHours(1));

0 commit comments

Comments
 (0)