Merge branch 'be/feat/120-autoLabeling' into 'be/develop'

Be/feat/120 autoLabeling

See merge request s11-s-project/S11P21S002!41
This commit is contained in:
김태수 2024-09-04 16:52:38 +09:00
commit 065ac8e334
20 changed files with 424 additions and 20 deletions

View File

@ -5,11 +5,14 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface FolderRepository extends JpaRepository<Folder, Integer> {
List<Folder> findAllByParentIsNull();
List<Folder> findAllByProjectIdAndParentIsNull(Integer projectId);
Optional<Folder> findAllByProjectIdAndId(Integer projectId, Integer folderId);
boolean existsByIdAndProjectId(Integer folderId, Integer projectId);
}

View File

@ -33,7 +33,7 @@ public class FolderService {
Folder parent = null;
if (folderRequest.getParentId() != 0) {
parent = getFolder(folderRequest.getParentId());
parent = getFolder(folderRequest.getParentId(),projectId);
}
Folder folder = Folder.of(folderRequest.getTitle(), parent, project);
@ -51,9 +51,9 @@ public class FolderService {
// 최상위 폴더
if (folderId == 0) {
return FolderResponse.from(folderRepository.findAllByParentIsNull());
return FolderResponse.from(folderRepository.findAllByProjectIdAndParentIsNull(projectId));
} else {
return FolderResponse.from(getFolder(folderId));
return FolderResponse.from(getFolder(folderId,projectId));
}
}
@ -62,7 +62,7 @@ public class FolderService {
*/
public FolderResponse updateFolder(final Integer memberId, final Integer projectId, final Integer folderId, final FolderRequest updatedFolderRequest) {
checkUnauthorized(memberId, projectId);
Folder folder = getFolder(folderId);
Folder folder = getFolder(folderId,projectId);
Folder parentFolder = folderRepository.findById(updatedFolderRequest.getParentId())
.orElse(null);
@ -77,7 +77,7 @@ public class FolderService {
*/
public void deleteFolder(final Integer memberId, final Integer projectId, final Integer folderId) {
checkUnauthorized(memberId, projectId);
Folder folder = getFolder(folderId);
Folder folder = getFolder(folderId,projectId);
folderRepository.delete(folder);
}
@ -86,8 +86,8 @@ public class FolderService {
.orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND));
}
private Folder getFolder(final Integer folderId) {
return folderRepository.findById(folderId)
private Folder getFolder(final Integer folderId, final Integer projectId) {
return folderRepository.findAllByProjectIdAndId(projectId,folderId)
.orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND));
}
@ -98,7 +98,7 @@ public class FolderService {
}
private void checkExistParticipant(final Integer memberId, final Integer projectId) {
if (participantRepository.existsByMemberIdAndProjectId(memberId, projectId)) {
if (!participantRepository.existsByMemberIdAndProjectId(memberId, projectId)) {
throw new CustomException(ErrorCode.BAD_REQUEST);
}
}

View File

@ -2,10 +2,20 @@ package com.worlabel.domain.image.repository;
import com.worlabel.domain.image.entity.Image;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface ImageRepository extends JpaRepository<Image, Long> {
Optional<Image> findByIdAndFolderId(Long imageId, Integer folderId);
// TODO: N + 1
@Query("select i from Image i " +
"join fetch i.folder f " +
"join fetch f.project p " +
"where p.id = :projectId")
List<Image> findImagesByProjectId(@Param("projectId") Integer projectId);
}

View File

@ -90,7 +90,7 @@ public class ImageService {
}
private void checkEditorParticipant(final Integer memberId, final Integer projectId) {
if (!participantRepository.doesParticipantUnauthorizedExistByMemberIdAndProjectId(projectId, memberId)) {
if (participantRepository.doesParticipantUnauthorizedExistByMemberIdAndProjectId(memberId,projectId)) {
throw new CustomException(ErrorCode.PARTICIPANT_UNAUTHORIZED);
}
}

View File

@ -0,0 +1,56 @@
package com.worlabel.domain.label.controller;
import com.worlabel.domain.label.service.LabelService;
import com.worlabel.global.annotation.CurrentUser;
import com.worlabel.global.config.swagger.SwaggerApiError;
import com.worlabel.global.config.swagger.SwaggerApiSuccess;
import com.worlabel.global.exception.ErrorCode;
import com.worlabel.global.response.BaseResponse;
import com.worlabel.global.response.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@Tag(name = "레이블링 관련 API")
@RequestMapping("/api/projects/{project_id}/label")
public class LabelController {
private final LabelService labelService;
@Operation(summary = "프로젝트 단위 오토레이블링", description = "해당 프로젝트 이미지를 오토레이블링합니다.")
@SwaggerApiSuccess(description = "해당 프로젝트가 오토 레이블링 됩니다.")
@SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR})
@PostMapping("/auto")
public BaseResponse<Void> projectAutoLabeling(
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId
) {
labelService.autoLabeling(projectId, memberId);
return SuccessResponse.empty();
}
@Operation(summary = "이미지 단위 레이블링", description = "진행한 레이블링을 저장합니다.")
@SwaggerApiSuccess(description = "해당 이미지에 대한 레이블링을 저장합니다.")
@SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR})
@PostMapping("/image/{image_id}")
public BaseResponse<Void> imageLabeling(
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId,
@PathVariable("image_id") final Integer imageId
) {
labelService.save(imageId);
return SuccessResponse.empty();
}
}

View File

@ -41,4 +41,12 @@ public class Label extends BaseEntity {
// @ManyToOne(fetch = FetchType.LAZY)
// @JoinColumn(name = "label_category_id")
// private LabelCategory labelCategory;
public static Label of(String jsonUrl, Image image) {
Label label = new Label();
label.url = jsonUrl;
label.image = image;
return label;
}
}

View File

@ -0,0 +1,26 @@
package com.worlabel.domain.label.entity.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import java.util.List;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AutoLabelingRequest {
@JsonProperty("project_id")
private Integer projectId;
@JsonProperty("image_list")
private List<ImageRequest> imageList;
// private Double confThreshold
// private Double iouThreshold;
// List<?> classes
public static AutoLabelingRequest of(final Integer projectId, final List<ImageRequest> imageList) {
return new AutoLabelingRequest(projectId, imageList);
}
}

View File

@ -0,0 +1,17 @@
package com.worlabel.domain.label.entity.dto;
import lombok.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AutoLabelingResponse {
private Long imageId;
private String imageUrl;
private String data; // JSON 형식의 데이터를 그대로 저장
public static AutoLabelingResponse of(Long imageId, String title, String data) {
return new AutoLabelingResponse(imageId, title, data);
}
}

View File

@ -0,0 +1,31 @@
package com.worlabel.domain.label.entity.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.worlabel.domain.image.entity.Image;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Schema(name = "오토 레이블링 요청 dto", description = "오토 레이블링 요청 DTO")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
@Getter
public class ImageRequest {
@Schema(description = "이미지 PK", example = "2")
@NotEmpty(message = "이미지 PK를 입력하세요")
@JsonProperty("image_id")
private Long imageId;
@Schema(description = "이미지 url", example = "image.png")
@NotEmpty(message = "이미지 url을 입력하세요")
@JsonProperty("image_url")
private String imageUrl;
public static ImageRequest of(Image image){
return new ImageRequest(image.getId(), image.getImageUrl());
}
}

View File

@ -0,0 +1,11 @@
package com.worlabel.domain.label.repository;
import com.worlabel.domain.label.entity.Label;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface LabelRepository extends JpaRepository<Label, Long> {
Optional<Label> findByImageId(Long imageId);
}

View File

@ -0,0 +1,165 @@
package com.worlabel.domain.label.service;
import com.google.gson.*;
import com.worlabel.domain.image.entity.Image;
import com.worlabel.domain.image.repository.ImageRepository;
import com.worlabel.domain.label.entity.Label;
import com.worlabel.domain.label.entity.dto.AutoLabelingRequest;
import com.worlabel.domain.label.entity.dto.AutoLabelingResponse;
import com.worlabel.domain.label.entity.dto.ImageRequest;
import com.worlabel.domain.label.repository.LabelRepository;
import com.worlabel.domain.participant.repository.ParticipantRepository;
import com.worlabel.domain.project.entity.ProjectType;
import com.worlabel.domain.project.repository.ProjectRepository;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
import com.worlabel.global.service.S3UploadService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class LabelService {
private final ParticipantRepository participantRepository;
private final ProjectRepository projectRepository;
private final LabelRepository labelRepository;
private final S3UploadService s3UploadService;
private final ImageRepository imageRepository;
private final RestTemplate restTemplate;
private final Gson gson;
/**
* AI SERVER 주소
*/
@Value("${ai.server}")
private String aiServer;
public void autoLabeling(final Integer projectId, final Integer memberId) {
checkEditorExistParticipant(memberId, projectId);
ProjectType projectType = getType(projectId);
log.debug("{}번 프로젝트 이미지 {} 진행 ", projectId, projectType);
List<Image> imageList = imageRepository.findImagesByProjectId(projectId);
List<ImageRequest> imageRequestList = imageList.stream().map(ImageRequest::of).toList();
AutoLabelingRequest autoLabelingRequest = AutoLabelingRequest.of(projectId, imageRequestList);
List<AutoLabelingResponse> autoLabelingResponseList = sendRequestToApi(autoLabelingRequest, projectType.getValue(), projectId);
for (int index = 0; index < autoLabelingResponseList.size(); index++) {
AutoLabelingResponse response = autoLabelingResponseList.get(index);
Image image = imageList.get(index);
String uploadedUrl = s3UploadService.uploadJson(response.getData(), response.getImageUrl());
Label label = labelRepository.findByImageId(response.getImageId())
.orElseGet(() -> Label.of(uploadedUrl, image));
labelRepository.save(label);
}
}
private List<AutoLabelingResponse> sendRequestToApi(AutoLabelingRequest autoLabelingRequest, String apiEndpoint, int projectId) {
String url = createApiUrl("api/yolo/detection/predict");
// RestTemplate을 동적으로 생성하여 사용
HttpHeaders headers = createJsonHeaders();
// 요청 본문 설정
HttpEntity<AutoLabelingRequest> request = new HttpEntity<>(autoLabelingRequest, headers);
try {
log.debug("요청 서버 : {}", url);
// AI 서버로 POST 요청
ResponseEntity<String> response = restTemplate.exchange(
url, // 요청을 보낼 URL
HttpMethod.POST, // HTTP 메서드 (POST)
request, // HTTP 요청 본문과 헤더가 포함된 객체
String.class // 응답을 String 타입으로
);
String responseBody = Optional.ofNullable(response.getBody())
.orElseThrow(() -> new CustomException(ErrorCode.AI_SERVER_ERROR));
log.info("AI 서버 응답 -> {}", response.getBody());
return parseAutoLabelingResponseList(responseBody);
} catch (Exception e) {
log.error("AI 서버 요청 중 오류 발생: ", e);
throw new CustomException(ErrorCode.AI_SERVER_ERROR);
}
}
private List<AutoLabelingResponse> parseAutoLabelingResponseList(String responseBody) {
JsonElement jsonElement = JsonParser.parseString(responseBody);
List<AutoLabelingResponse> autoLabelingResponseList = new ArrayList<>();
for (JsonElement element : jsonElement.getAsJsonArray()) {
AutoLabelingResponse response = parseAutoLabelingResponse(element);
autoLabelingResponseList.add(response);
}
return autoLabelingResponseList;
}
/**
* jsonElement -> AutoLabelingResponse
*/
private AutoLabelingResponse parseAutoLabelingResponse(JsonElement element) {
JsonObject jsonObject = element.getAsJsonObject();
Long imageId = jsonObject.get("image_id").getAsLong();
String imageUrl = jsonObject.get("image_url").getAsString();
JsonObject data = jsonObject.get("data").getAsJsonObject();
return AutoLabelingResponse.of(imageId, imageUrl, gson.toJson(data));
}
/**
* API URL 구성
*/
private String createApiUrl(String endPoint) {
return aiServer + "/" + endPoint;
}
/**
* 요청 헤더 설정
*/
private static HttpHeaders createJsonHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
return headers;
}
/**
* 프로젝트 타입 조회
*/
private ProjectType getType(final Integer projectId) {
return projectRepository.findProjectTypeById(projectId)
.orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND));
}
/**
* 참여자(EDITOR, ADMIN) 검증 메서드
*/
private void checkEditorExistParticipant(final Integer memberId, final Integer projectId) {
if (participantRepository.doesParticipantUnauthorizedExistByMemberIdAndProjectId(memberId, projectId)) {
throw new CustomException(ErrorCode.PARTICIPANT_UNAUTHORIZED);
}
}
// TODO : 구현
public void save(final Integer imageId) {
}
}

View File

@ -119,4 +119,5 @@ public class ProjectController {
projectService.removeProjectMember(memberId, projectId, removeMemberId);
return SuccessResponse.empty();
}
}

View File

@ -21,7 +21,7 @@ public class ProjectResponse {
private String title;
@Schema(description = "워크스페이스 id", example = "1")
private Integer projectId;
private Integer workspaceId;
@Schema(description = "프로젝트 타입", example = "classification")
private ProjectType projectType;

View File

@ -1,12 +1,15 @@
package com.worlabel.domain.project.repository;
import com.worlabel.domain.project.entity.Project;
import com.worlabel.domain.project.entity.ProjectType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import javax.swing.text.html.Option;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProjectRepository extends JpaRepository<Project, Integer> {
@ -24,4 +27,8 @@ public interface ProjectRepository extends JpaRepository<Project, Integer> {
@Param("memberId") Integer memberId,
@Param("lastProjectId") Integer lastProjectId,
@Param("pageSize") Integer pageSize);
// ProjectType을 가져오는 메서드 추가
@Query("SELECT p.projectType FROM Project p WHERE p.id = :projectId")
Optional<ProjectType> findProjectTypeById(@Param("projectId") Integer projectId);
}

View File

@ -17,15 +17,18 @@ import com.worlabel.domain.workspace.repository.WorkspaceRepository;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
@RequiredArgsConstructor
public class ProjectService {
private final ProjectRepository projectRepository;
@ -34,6 +37,7 @@ public class ProjectService {
private final MemberRepository memberRepository;
private final WorkspaceParticipantRepository workspaceParticipantRepository;
public ProjectResponse createProject(final Integer memberId, final Integer workspaceId, final ProjectRequest projectRequest) {
Workspace workspace = getWorkspace(memberId, workspaceId);
Member member = getMember(memberId);
@ -107,6 +111,7 @@ public class ProjectService {
participantRepository.delete(participant);
}
private Workspace getWorkspace(final Integer memberId, final Integer workspaceId) {
return workspaceRepository.findByMemberIdAndId(memberId, workspaceId)
.orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND));
@ -146,3 +151,4 @@ public class ProjectService {
}
}
}

View File

@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.lang.reflect.Executable;
import java.util.Enumeration;
@Slf4j
@ -57,6 +59,13 @@ public class CustomControllerAdvice {
return ErrorResponse.of(new CustomException(ErrorCode.EMPTY_REQUEST_PARAMETER));
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ErrorResponse handleRequestMethodNotSupportedException(Exception e, HttpServletRequest request) {
log.error("", e);
sendNotification(e, request);
return ErrorResponse.of(new CustomException(ErrorCode.BAD_REQUEST, "지원하지 않는 API입니다. 요청을 확인해주세요"));
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e, HttpServletRequest request) {
log.error("", e);
@ -65,9 +74,11 @@ public class CustomControllerAdvice {
.body(ErrorResponse.of(e));
}
private void sendNotification(Exception e, HttpServletRequest request) {
// TODO: 추후 주석 해제
notificationManager.sendNotification(e, request.getRequestURI(),getParams(request));
// notificationManager.sendNotification(e, request.getRequestURI(),getParams(request));
}
private String getParams(HttpServletRequest req) {

View File

@ -64,8 +64,11 @@ public class SecurityConfig {
// 경로별 인가 작업
http
.authorizeHttpRequests(auth->auth
.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/auth/reissue").permitAll()
.anyRequest().authenticated());
.anyRequest().authenticated()
// .anyRequest().permitAll()
);
// OAuth2
http

View File

@ -2,7 +2,9 @@ package com.worlabel.global.config;
import com.worlabel.global.resolver.CurrentUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -18,4 +20,9 @@ public class WebConfig implements WebMvcConfigurer {
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@ -16,6 +16,7 @@ public enum ErrorCode {
INVALID_URL(HttpStatus.BAD_REQUEST, 1004, "제공하지 않는 주소입니다. 확인해주세요"),
FAIL_TO_CREATE_FILE(HttpStatus.BAD_REQUEST, 1005, "파일 업로드에 실패하였습니다. 다시 한번 확인해주세요"),
FAIL_TO_DELETE_FILE(HttpStatus.BAD_REQUEST, 1006, "파일 삭제에 실패하였습니다. 다시 한번 확인해주세요"),
INVALID_FILE_PATH(HttpStatus.BAD_REQUEST,1007 , "파일 경로가 잘못되었습니다. 다시 한번 확인해주세요"),
// Auth & Member - 2000
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
@ -47,8 +48,10 @@ public enum ErrorCode {
// Image - 7000
IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, 7000,"해당 이미지를 찾을 수 없습니다."),
;
// AI - 8000
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 8000, "AI 서버 오류 입니다."),
;
private final HttpStatus status;
private final int code;
private final String message;

View File

@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
@ -22,6 +23,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.UUID;
// TODO: 추후 비동기로 변경해야합니다.
@Slf4j
@Service
@RequiredArgsConstructor
@ -44,6 +46,32 @@ public class S3UploadService {
@Value("${cloud.aws.url}")
private String url;
public String uploadJson(final String json, final String imageUrl) {
// String targetUrl = projectId + "/" + title + ".json"; // S3에 업로드할 대상 URL
String targetUrl = removeExtension(getKeyFromImageAddress(imageUrl)) + ".json";
log.debug("주소 {}", targetUrl);
try {
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("application/json");
metadata.setContentLength(jsonBytes.length);
// JSON 데이터를 S3에 업로드
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonBytes)) {
PutObjectRequest putRequest = new PutObjectRequest(bucket, targetUrl, inputStream, metadata);
amazonS3.putObject(putRequest); // S3에 파일 업로드
}
URL uploadedUrl = amazonS3.getUrl(bucket, targetUrl);
log.debug("Uploaded JSON URL: {}", uploadedUrl);
return uploadedUrl.toString(); // 업로드된 파일의 URL 반환
} catch (Exception e) {
log.error("JSON 업로드 중 오류 발생: ", e);
throw new CustomException(ErrorCode.FAIL_TO_CREATE_FILE);
}
}
/**
* 파일이 존재하는지 확인
@ -60,7 +88,7 @@ public class S3UploadService {
*/
private String uploadImage(final MultipartFile image, final Integer projectId) {
try {
return uploadToS3(image, projectId);
return uploadImageToS3(image, projectId);
} catch (IOException e) {
throw new CustomException(ErrorCode.FAIL_TO_CREATE_FILE);
}
@ -69,7 +97,7 @@ public class S3UploadService {
/**
* AWS S3 이미지 업로드
*/
private String uploadToS3(final MultipartFile image, final Integer projectId) throws IOException {
private String uploadImageToS3(final MultipartFile image, final Integer projectId) throws IOException {
String originalFileName = image.getOriginalFilename(); // 원본 파일 이름
String extension = originalFileName.substring(originalFileName.lastIndexOf(".") + 1); // 파일 확장자
@ -99,11 +127,11 @@ public class S3UploadService {
return url.getPath();
}
private static String getS3FileName(Integer projectId, String extension) {
private static String getS3FileName(final Integer projectId, final String extension) {
return projectId + "/" + UUID.randomUUID().toString().substring(0, 13) + "." + extension;
}
public void deleteImageFromS3(String imageAddress){
public void deleteImageFromS3(String imageAddress) {
String key = getKeyFromImageAddress(imageAddress);
try {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, key));
@ -112,13 +140,24 @@ public class S3UploadService {
}
}
private String removeExtension(String url) {
if (!StringUtils.hasText(url) || !url.contains(".")) {
return url;
}
return url.substring(0, url.lastIndexOf("."));
}
private String getKeyFromImageAddress(String imageAddress) {
if (!StringUtils.hasText(imageAddress)) {
throw new CustomException(ErrorCode.INVALID_FILE_PATH);
}
try {
URL url = new URL(imageAddress);
String decodingKey = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
return decodingKey.substring(1);
} catch (MalformedURLException e) {
throw new CustomException(ErrorCode.FAIL_TO_DELETE_FILE);
throw new CustomException(ErrorCode.INVALID_FILE_PATH);
}
}
}