Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
import com.back.global.rq.Rq;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand Down Expand Up @@ -73,14 +77,16 @@ public RsData<MentoringResponse> getMentoring(
);
}

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

return new RsData<>(
"201",
Expand All @@ -89,14 +95,17 @@ public RsData<MentoringResponse> createMentoring(
);
}

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

return new RsData<>(
"200",
Expand All @@ -106,6 +115,7 @@ public RsData<MentoringResponse> updateMentoring(
}

@DeleteMapping("/{mentoringId}")
@PreAuthorize("hasRole('MENTOR')")
@Operation(summary = "멘토링 삭제", description = "멘토링을 삭제합니다. 멘토링 작성자만 접근할 수 있습니다.")
public RsData<Void> deleteMentoring(
@PathVariable Long mentoringId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ public record MentoringRequest(

@Schema(description = "멘토링 소개", example = "bio")
@NotNull
String bio,

@Schema(description = "멘토링 썸네일", example = "test.png")
String thumb
String bio
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,15 @@ public class Mentoring extends BaseEntity {
private double rating = 0.0;

@Builder
public Mentoring(Mentor mentor, String title, String bio, String thumb) {
public Mentoring(Mentor mentor, String title, String bio) {
this.mentor = mentor;
this.title = title;
this.bio = bio;
this.thumb = thumb;
}

public void update(String title, String bio, List<Tag> tags, String thumb) {
public void update(String title, String bio, List<Tag> tags) {
this.title = title;
this.bio = bio;
this.thumb = thumb;

updateTags(tags);
}
Expand All @@ -59,6 +57,10 @@ public void updateTags(List<Tag> tags) {
}
}

public void updateThumb(String thumb) {
this.thumb = thumb;
}

public void updateRating(double averageRating) {
this.rating = averageRating;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.back.domain.mentoring.mentoring.error;

import com.back.global.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ImageErrorCode implements ErrorCode {
// 400
FILE_SIZE_EXCEEDED("400-1", "이미지 파일 크기는 10MB를 초과할 수 없습니다."),
INVALID_FILE_TYPE("400-2", "이미지 파일만 업로드 가능합니다."),
UNSUPPORTED_IMAGE_FORMAT("400-3", "JPG, PNG 형식만 업로드 가능합니다."),
IMAGE_UPLOAD_FAILED("400-4", "이미지 업로드에 실패했습니다.");

private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse;
import com.back.domain.mentoring.mentoring.entity.Mentoring;
import com.back.domain.mentoring.mentoring.entity.Tag;
import com.back.domain.mentoring.mentoring.error.ImageErrorCode;
import com.back.domain.mentoring.mentoring.error.MentoringErrorCode;
import com.back.domain.mentoring.mentoring.repository.MentoringRepository;
import com.back.domain.mentoring.mentoring.repository.TagRepository;
Expand All @@ -18,7 +19,9 @@
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
Expand All @@ -30,6 +33,7 @@ public class MentoringService {
private final MentoringRepository mentoringRepository;
private final MentoringStorage mentoringStorage;
private final TagRepository tagRepository;
private final S3ImageUploader s3ImageUploader;

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

@Transactional
public MentoringResponse createMentoring(MentoringRequest reqDto, Mentor mentor) {
public MentoringResponse createMentoring(MentoringRequest reqDto, MultipartFile thumb, Mentor mentor) {
validateMentoringTitle(mentor.getId(), reqDto.title());

Mentoring mentoring = Mentoring.builder()
.mentor(mentor)
.title(reqDto.title())
.bio(reqDto.bio())
.thumb(reqDto.thumb())
.build();

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

mentoringRepository.save(mentoring);
mentoringRepository.saveAndFlush(mentoring);

uploadThumb(thumb, mentoring);

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

@Transactional
public MentoringResponse updateMentoring(Long mentoringId, MentoringRequest reqDto, Mentor mentor) {
public MentoringResponse updateMentoring(Long mentoringId, MentoringRequest reqDto, MultipartFile thumb, Mentor mentor) {
Mentoring mentoring = mentoringStorage.findMentoring(mentoringId);

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

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

mentoring.update(reqDto.title(), reqDto.bio(), tags, reqDto.thumb());
mentoring.update(reqDto.title(), reqDto.bio(), tags);
uploadThumb(thumb, mentoring);

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


// ===== 썸네일 =====

private void uploadThumb(MultipartFile thumb, Mentoring mentoring) {
if (thumb != null && !thumb.isEmpty()) {
String imageUrl = null;
try {
String path = "mentoring/" + mentoring.getId();
imageUrl = s3ImageUploader.upload(thumb, path);
} catch (IOException e) {
throw new ServiceException(ImageErrorCode.IMAGE_UPLOAD_FAILED);
}

mentoring.updateThumb(imageUrl);
}
}


// ===== 유효성 검사 =====

private void validateOwner(Mentoring mentoring, Mentor mentor) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.back.domain.mentoring.mentoring.service;

import com.back.domain.mentoring.mentoring.error.ImageErrorCode;
import com.back.global.exception.ServiceException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.Set;

@Component
@RequiredArgsConstructor
public class S3ImageUploader {

private final S3Client s3Client;

@Value("${aws.s3.bucket}")
private String bucket;

private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
private static final Set<String> ALLOWED_TYPES = Set.of("image/jpeg", "image/jpg", "image/png", "image/webp");
private static final String IMAGE_BASE_PATH = "images/";

public String upload(MultipartFile file, String path) throws IOException {
validateImageFile(file);

String fullPath = IMAGE_BASE_PATH + path;

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(fullPath)
.contentType(file.getContentType())
.build();

s3Client.putObject(
putObjectRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize())
);

return s3Client.utilities()
.getUrl(builder -> builder.bucket(bucket).key(fullPath))
.toString();
}

private void validateImageFile(MultipartFile image) {
if (image.getSize() > MAX_FILE_SIZE) {
throw new ServiceException(ImageErrorCode.FILE_SIZE_EXCEEDED);
}

String contentType = image.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new ServiceException(ImageErrorCode.INVALID_FILE_TYPE);
}
if (!ALLOWED_TYPES.contains(contentType)) {
throw new ServiceException(ImageErrorCode.INVALID_FILE_TYPE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import com.back.domain.mentoring.slot.service.MentorSlotService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

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

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