From 3d8d766a042982addff67b1a79c1b51f19154c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=88=98?= Date: Tue, 3 Sep 2024 16:57:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20Auto=20=EB=9D=BC=EB=B2=A8=EB=A7=81?= =?UTF-8?q?=20API-S11P21S002-120?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../folder/repository/FolderRepository.java | 2 +- .../domain/folder/service/FolderService.java | 4 +- .../image/repository/ImageRepository.java | 10 ++ .../domain/image/service/ImageService.java | 2 +- .../label/controller/LabelController.java | 41 +++++++ .../entity/dto/AutoLabelingResponse.java | 10 ++ .../domain/label/entity/dto/ImageRequest.java | 35 ++++++ .../domain/label/service/LabelService.java | 116 ++++++++++++++++++ .../project/controller/ProjectController.java | 1 + .../project/entity/dto/ProjectResponse.java | 2 +- .../project/repository/ProjectRepository.java | 7 ++ .../project/service/ProjectService.java | 8 +- .../global/advice/CustomControllerAdvice.java | 13 +- .../worlabel/global/exception/ErrorCode.java | 3 + .../global/service/S3UploadService.java | 30 ++++- 15 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java create mode 100644 backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java create mode 100644 backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java create mode 100644 backend/src/main/java/com/worlabel/domain/label/service/LabelService.java diff --git a/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java b/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java index 4ee318f..ae0bd3a 100644 --- a/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java +++ b/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java @@ -9,7 +9,7 @@ import java.util.List; @Repository public interface FolderRepository extends JpaRepository { - List findAllByParentIsNull(); + List findAllByProjectIdAndParentIsNull(Integer projectId); boolean existsByIdAndProjectId(Integer folderId, Integer projectId); } \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java b/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java index 5a5ca4a..11834e3 100644 --- a/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java +++ b/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java @@ -51,7 +51,7 @@ public class FolderService { // 최상위 폴더 if (folderId == 0) { - return FolderResponse.from(folderRepository.findAllByParentIsNull()); + return FolderResponse.from(folderRepository.findAllByProjectIdAndParentIsNull(projectId)); } else { return FolderResponse.from(getFolder(folderId)); } @@ -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); } } diff --git a/backend/src/main/java/com/worlabel/domain/image/repository/ImageRepository.java b/backend/src/main/java/com/worlabel/domain/image/repository/ImageRepository.java index b20e787..2e70391 100644 --- a/backend/src/main/java/com/worlabel/domain/image/repository/ImageRepository.java +++ b/backend/src/main/java/com/worlabel/domain/image/repository/ImageRepository.java @@ -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 { Optional 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 findImagesByProjectId(@Param("projectId") Integer projectId); } diff --git a/backend/src/main/java/com/worlabel/domain/image/service/ImageService.java b/backend/src/main/java/com/worlabel/domain/image/service/ImageService.java index 53a9242..0f93361 100644 --- a/backend/src/main/java/com/worlabel/domain/image/service/ImageService.java +++ b/backend/src/main/java/com/worlabel/domain/image/service/ImageService.java @@ -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); } } diff --git a/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java b/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java new file mode 100644 index 0000000..3b4e3c7 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java @@ -0,0 +1,41 @@ +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 projectAutoLabeling( + @CurrentUser final Integer memberId, + @PathVariable("project_id") final Integer projectId + ) { + labelService.autoLabeling(projectId, memberId); + return SuccessResponse.empty(); + } + +} diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java b/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java new file mode 100644 index 0000000..14093f0 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java @@ -0,0 +1,10 @@ +package com.worlabel.domain.label.entity.dto; + +import lombok.Data; + +@Data +public class AutoLabelingResponse { + private String image_id; + private String title; + private String data; // JSON 형식의 데이터를 그대로 저장 +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java b/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java new file mode 100644 index 0000000..d934a5b --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java @@ -0,0 +1,35 @@ +package com.worlabel.domain.label.entity.dto; + +import com.worlabel.domain.image.entity.Image; +import com.worlabel.domain.project.entity.ProjectType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +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를 입력하세요") + private Long id; + + // TODO: Title 들어가야 함 +// @Schema(description = "이미지 PK", example = "2") +// @NotEmpty(message = "이미지 PK를 입력하세요") +// private String title; + + @Schema(description = "프로젝트 유형", example = "classification") + @NotNull(message = "카테고리를 입력하세요.") + private ProjectType projectType; + + public static ImageRequest of(Image image, ProjectType projectType){ + return new ImageRequest(image.getId(), projectType); + } +} diff --git a/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java b/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java new file mode 100644 index 0000000..f163af5 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java @@ -0,0 +1,116 @@ +package com.worlabel.domain.label.service; + +import com.worlabel.domain.image.repository.ImageRepository; +import com.worlabel.domain.label.entity.dto.AutoLabelingResponse; +import com.worlabel.domain.label.entity.dto.ImageRequest; +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.core.ParameterizedTypeReference; +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.web.client.RestTemplate; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LabelService { + + private final ParticipantRepository participantRepository; + private final RestTemplateBuilder restTemplateBuilder; + private final ProjectRepository projectRepository; + private final S3UploadService s3UploadService; + private final ImageRepository imageRepository; + + /** + * AI SERVER 주소 + */ + @Value("${ai.server}") + private String aiServer; + + public void autoLabeling(final Integer projectId, final Integer memberId) { + checkEditorExistParticipant(memberId, projectId); + + ProjectType projectType = getType(projectId); + List imageRequestList = getImageRequestList(projectId, projectType); + + List autoLabelingResponseList = sendRequestToApi(imageRequestList, projectType.getValue(), projectId); + } + + private List sendRequestToApi(List imageRequestList, String apiEndpoint, int projectId) { + String url = aiServer + "/" + apiEndpoint; + // 요청 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + + // 요청 본문 설정 + HttpEntity> request = new HttpEntity<>(imageRequestList, headers); + + // RestTemplate을 동적으로 생성하여 사용 + RestTemplate restTemplate = restTemplateBuilder.build(); + try { + // AI 서버로 POST 요청 + // TODO: 응답 추후 교체 + ResponseEntity> response = restTemplate.exchange( + url, // 요청을 보낼 URL + HttpMethod.POST, // HTTP 메서드 (POST) + request, // HTTP 요청 본문과 헤더가 포함된 객체 + new ParameterizedTypeReference>() { + } // 응답 타입을 지정 + ); + + log.info("AI 서버 응답 -> {}", response.getBody()); + // JSON 응답을 S3에 업로드 + if(response.getBody() == null) { + throw new CustomException(ErrorCode.AI_SERVER_ERROR); + } +// if (response.getBody() != null) { +// for (AutoLabelingResponse autoLabelingResponse : response.getBody()) { +// String imageId = autoLabelingResponse.getImage_id(); +// String jsonData = autoLabelingResponse.getData(); +// String title = autoLabelingResponse.getTitle(); +// if (imageId != null && jsonData != null) { +// // TODO: 잘 받아온다면 s3에 업로드 +// log.debug("구현 무리없이 잘 된 경우 :{}", autoLabelingResponse); +//// String jsonUrl = s3UploadService.uploadJson(jsonData, title, projectId); +//// DB에 저장해야한다. -> 레이블이 있다면 저장 없다면 생성 해야한다. +// } +// } +// // 이 곳에서 리턴 후 다른 곳에서 넣는게 코드가 더 깔끔해질 것 같다. + return response.getBody(); +// } + } catch (Exception e) { + log.error("AI 서버 요청 중 오류 발생: ", e); + throw new CustomException(ErrorCode.AI_SERVER_ERROR); + } + } + + // TODO: N + 1문제 발생 추후 리팩토링해야합니다. + private List getImageRequestList(Integer projectId, ProjectType projectType) { + return imageRepository.findImagesByProjectId(projectId) + .stream().map(o -> ImageRequest.of(o, projectType)).toList(); + } + + private ProjectType getType(final Integer projectId) { + return projectRepository.findProjectTypeById(projectId) + .orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND)); + } + + private void checkEditorExistParticipant(final Integer memberId, final Integer projectId) { + if (participantRepository.doesParticipantUnauthorizedExistByMemberIdAndProjectId(memberId, projectId)) { + throw new CustomException(ErrorCode.PARTICIPANT_UNAUTHORIZED); + } + } +} diff --git a/backend/src/main/java/com/worlabel/domain/project/controller/ProjectController.java b/backend/src/main/java/com/worlabel/domain/project/controller/ProjectController.java index 3a78ec1..da3701b 100644 --- a/backend/src/main/java/com/worlabel/domain/project/controller/ProjectController.java +++ b/backend/src/main/java/com/worlabel/domain/project/controller/ProjectController.java @@ -119,4 +119,5 @@ public class ProjectController { projectService.removeProjectMember(memberId, projectId, removeMemberId); return SuccessResponse.empty(); } + } diff --git a/backend/src/main/java/com/worlabel/domain/project/entity/dto/ProjectResponse.java b/backend/src/main/java/com/worlabel/domain/project/entity/dto/ProjectResponse.java index adffafa..379df59 100644 --- a/backend/src/main/java/com/worlabel/domain/project/entity/dto/ProjectResponse.java +++ b/backend/src/main/java/com/worlabel/domain/project/entity/dto/ProjectResponse.java @@ -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; diff --git a/backend/src/main/java/com/worlabel/domain/project/repository/ProjectRepository.java b/backend/src/main/java/com/worlabel/domain/project/repository/ProjectRepository.java index 7541909..a83db99 100644 --- a/backend/src/main/java/com/worlabel/domain/project/repository/ProjectRepository.java +++ b/backend/src/main/java/com/worlabel/domain/project/repository/ProjectRepository.java @@ -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 { @@ -24,4 +27,8 @@ public interface ProjectRepository extends JpaRepository { @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 findProjectTypeById(@Param("projectId") Integer projectId); } diff --git a/backend/src/main/java/com/worlabel/domain/project/service/ProjectService.java b/backend/src/main/java/com/worlabel/domain/project/service/ProjectService.java index 390e9b6..55b9e1d 100644 --- a/backend/src/main/java/com/worlabel/domain/project/service/ProjectService.java +++ b/backend/src/main/java/com/worlabel/domain/project/service/ProjectService.java @@ -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 { } } } + diff --git a/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java b/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java index 170fd07..f97f412 100644 --- a/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java +++ b/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java @@ -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 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) { diff --git a/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java b/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java index 0454786..2372bcd 100644 --- a/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java +++ b/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java @@ -47,6 +47,9 @@ 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; diff --git a/backend/src/main/java/com/worlabel/global/service/S3UploadService.java b/backend/src/main/java/com/worlabel/global/service/S3UploadService.java index 4b03143..67010c8 100644 --- a/backend/src/main/java/com/worlabel/global/service/S3UploadService.java +++ b/backend/src/main/java/com/worlabel/global/service/S3UploadService.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.UUID; +// TODO: 추후 비동기로 변경해야합니다. @Slf4j @Service @RequiredArgsConstructor @@ -44,6 +45,29 @@ public class S3UploadService { @Value("${cloud.aws.url}") private String url; + public String uploadJson(final String json, final String title, final Integer projectId) { + String targetUrl = projectId + "/" + title + ".json"; // S3에 업로드할 대상 URL + + 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 +84,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 +93,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,7 +123,7 @@ 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; } From 971c3fb6fd8d453ec2e3ae0391ff448f86820b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=88=98?= Date: Tue, 3 Sep 2024 17:06:03 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20T?= =?UTF-8?q?itle=20->=20ImageUrl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/label/controller/LabelController.java | 15 +++++++++++++++ .../domain/label/entity/dto/ImageRequest.java | 11 +++++------ .../domain/label/service/LabelService.java | 3 +++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java b/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java index 3b4e3c7..da1ad65 100644 --- a/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java +++ b/backend/src/main/java/com/worlabel/domain/label/controller/LabelController.java @@ -38,4 +38,19 @@ public class LabelController { return SuccessResponse.empty(); } + @Operation(summary = "이미지 단위 레이블링", description = "진행한 레이블링을 저장합니다.") + @SwaggerApiSuccess(description = "해당 이미지에 대한 레이블링을 저장합니다.") + @SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR}) + @PostMapping("/image/{image_id}") + public BaseResponse imageLabeling( + @CurrentUser final Integer memberId, + @PathVariable("project_id") final Integer projectId, + @PathVariable("image_id") final Integer imageId + ) { + labelService.save(imageId); + return SuccessResponse.empty(); + } + + + } diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java b/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java index d934a5b..2911d16 100644 --- a/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java +++ b/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java @@ -19,17 +19,16 @@ public class ImageRequest { @Schema(description = "이미지 PK", example = "2") @NotEmpty(message = "이미지 PK를 입력하세요") private Long id; - - // TODO: Title 들어가야 함 -// @Schema(description = "이미지 PK", example = "2") -// @NotEmpty(message = "이미지 PK를 입력하세요") -// private String title; + + @Schema(description = "이미지 url", example = "image.png") + @NotEmpty(message = "이미지 url을 입력하세요") + private String imageUrl; @Schema(description = "프로젝트 유형", example = "classification") @NotNull(message = "카테고리를 입력하세요.") private ProjectType projectType; public static ImageRequest of(Image image, ProjectType projectType){ - return new ImageRequest(image.getId(), projectType); + return new ImageRequest(image.getId(), image.getImageUrl(), projectType); } } diff --git a/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java b/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java index f163af5..a2ded9a 100644 --- a/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java +++ b/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java @@ -113,4 +113,7 @@ public class LabelService { throw new CustomException(ErrorCode.PARTICIPANT_UNAUTHORIZED); } } + + public void save(final Integer imageId) { + } } From bc8988296c1552c94ffc8355964afbdcd3b445f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=88=98?= Date: Wed, 4 Sep 2024 14:20:54 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Feat:=20=EC=98=A4=ED=86=A0=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EB=A7=81=20AI=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../folder/repository/FolderRepository.java | 3 + .../domain/folder/service/FolderService.java | 12 +- .../label/entity/dto/AutoLabelingRequest.java | 26 ++++ .../entity/dto/AutoLabelingResponse.java | 15 ++- .../domain/label/entity/dto/ImageRequest.java | 15 +-- .../domain/label/service/LabelService.java | 119 ++++++++++++------ .../global/config/SecurityConfig.java | 5 +- .../com/worlabel/global/config/WebConfig.java | 7 ++ 8 files changed, 142 insertions(+), 60 deletions(-) create mode 100644 backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingRequest.java diff --git a/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java b/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java index ae0bd3a..32e43cd 100644 --- a/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java +++ b/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java @@ -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 { List findAllByProjectIdAndParentIsNull(Integer projectId); + Optional findAllByProjectIdAndId(Integer projectId, Integer folderId); + boolean existsByIdAndProjectId(Integer folderId, Integer projectId); } \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java b/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java index 11834e3..87c669d 100644 --- a/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java +++ b/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java @@ -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); @@ -53,7 +53,7 @@ public class FolderService { if (folderId == 0) { 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)); } diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingRequest.java b/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingRequest.java new file mode 100644 index 0000000..1589603 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingRequest.java @@ -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 imageList; + + // private Double confThreshold + // private Double iouThreshold; + // List classes + + public static AutoLabelingRequest of(final Integer projectId, final List imageList) { + return new AutoLabelingRequest(projectId, imageList); + } +} diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java b/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java index 14093f0..279318a 100644 --- a/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java +++ b/backend/src/main/java/com/worlabel/domain/label/entity/dto/AutoLabelingResponse.java @@ -1,10 +1,17 @@ package com.worlabel.domain.label.entity.dto; -import lombok.Data; +import lombok.*; -@Data +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class AutoLabelingResponse { - private String image_id; - private String title; + + 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); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java b/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java index 2911d16..e87fc2c 100644 --- a/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java +++ b/backend/src/main/java/com/worlabel/domain/label/entity/dto/ImageRequest.java @@ -1,10 +1,9 @@ package com.worlabel.domain.label.entity.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import com.worlabel.domain.image.entity.Image; -import com.worlabel.domain.project.entity.ProjectType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,17 +17,15 @@ public class ImageRequest { @Schema(description = "이미지 PK", example = "2") @NotEmpty(message = "이미지 PK를 입력하세요") - private Long id; + @JsonProperty("image_id") + private Long imageId; @Schema(description = "이미지 url", example = "image.png") @NotEmpty(message = "이미지 url을 입력하세요") + @JsonProperty("image_url") private String imageUrl; - @Schema(description = "프로젝트 유형", example = "classification") - @NotNull(message = "카테고리를 입력하세요.") - private ProjectType projectType; - - public static ImageRequest of(Image image, ProjectType projectType){ - return new ImageRequest(image.getId(), image.getImageUrl(), projectType); + public static ImageRequest of(Image image){ + return new ImageRequest(image.getId(), image.getImageUrl()); } } diff --git a/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java b/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java index a2ded9a..9048cdc 100644 --- a/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java +++ b/backend/src/main/java/com/worlabel/domain/label/service/LabelService.java @@ -1,6 +1,8 @@ package com.worlabel.domain.label.service; +import com.google.gson.*; import com.worlabel.domain.image.repository.ImageRepository; +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.participant.repository.ParticipantRepository; @@ -13,7 +15,6 @@ 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.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -21,7 +22,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Slf4j @Service @@ -29,10 +32,13 @@ import java.util.List; public class LabelService { private final ParticipantRepository participantRepository; - private final RestTemplateBuilder restTemplateBuilder; +// private final RestTemplateBuilder restTemplateBuilder; + private final RestTemplate restTemplate; private final ProjectRepository projectRepository; private final S3UploadService s3UploadService; private final ImageRepository imageRepository; + private final Gson gson; + /** * AI SERVER 주소 @@ -44,76 +50,109 @@ public class LabelService { checkEditorExistParticipant(memberId, projectId); ProjectType projectType = getType(projectId); - List imageRequestList = getImageRequestList(projectId, projectType); + log.debug("{}번 프로젝트 이미지 {} 진행 ", projectId, projectType); - List autoLabelingResponseList = sendRequestToApi(imageRequestList, projectType.getValue(), projectId); + List imageRequestList = getImageRequestList(projectId); + AutoLabelingRequest autoLabelingRequest = AutoLabelingRequest.of(projectId, imageRequestList); + + List autoLabelingResponseList = sendRequestToApi(autoLabelingRequest, projectType.getValue(), projectId); } - private List sendRequestToApi(List imageRequestList, String apiEndpoint, int projectId) { - String url = aiServer + "/" + apiEndpoint; - // 요청 헤더 설정 - HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/json"); - - // 요청 본문 설정 - HttpEntity> request = new HttpEntity<>(imageRequestList, headers); + private List sendRequestToApi(AutoLabelingRequest autoLabelingRequest, String apiEndpoint, int projectId) { + String url = createApiUrl("api/yolo/detection/predict"); // RestTemplate을 동적으로 생성하여 사용 - RestTemplate restTemplate = restTemplateBuilder.build(); + HttpHeaders headers = createJsonHeaders(); + + // 요청 본문 설정 + HttpEntity request = new HttpEntity<>(autoLabelingRequest, headers); + try { + log.debug("요청 서버 : {}", url); // AI 서버로 POST 요청 - // TODO: 응답 추후 교체 - ResponseEntity> response = restTemplate.exchange( + ResponseEntity response = restTemplate.exchange( url, // 요청을 보낼 URL HttpMethod.POST, // HTTP 메서드 (POST) request, // HTTP 요청 본문과 헤더가 포함된 객체 - new ParameterizedTypeReference>() { - } // 응답 타입을 지정 + String.class // 응답을 String 타입으로 ); + String responseBody = Optional.ofNullable(response.getBody()) + .orElseThrow(() -> new CustomException(ErrorCode.AI_SERVER_ERROR)); + log.info("AI 서버 응답 -> {}", response.getBody()); - // JSON 응답을 S3에 업로드 - if(response.getBody() == null) { - throw new CustomException(ErrorCode.AI_SERVER_ERROR); - } -// if (response.getBody() != null) { -// for (AutoLabelingResponse autoLabelingResponse : response.getBody()) { -// String imageId = autoLabelingResponse.getImage_id(); -// String jsonData = autoLabelingResponse.getData(); -// String title = autoLabelingResponse.getTitle(); -// if (imageId != null && jsonData != null) { -// // TODO: 잘 받아온다면 s3에 업로드 -// log.debug("구현 무리없이 잘 된 경우 :{}", autoLabelingResponse); -//// String jsonUrl = s3UploadService.uploadJson(jsonData, title, projectId); -//// DB에 저장해야한다. -> 레이블이 있다면 저장 없다면 생성 해야한다. -// } -// } -// // 이 곳에서 리턴 후 다른 곳에서 넣는게 코드가 더 깔끔해질 것 같다. - return response.getBody(); -// } + + return parseAutoLabelingResponseList(responseBody); } catch (Exception e) { log.error("AI 서버 요청 중 오류 발생: ", e); throw new CustomException(ErrorCode.AI_SERVER_ERROR); } } - // TODO: N + 1문제 발생 추후 리팩토링해야합니다. - private List getImageRequestList(Integer projectId, ProjectType projectType) { - return imageRepository.findImagesByProjectId(projectId) - .stream().map(o -> ImageRequest.of(o, projectType)).toList(); + + private List parseAutoLabelingResponseList(String responseBody) { + JsonElement jsonElement = JsonParser.parseString(responseBody); + List 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; + } + + // TODO: N + 1문제 발생 추후 리팩토링해야합니다. + private List getImageRequestList(Integer projectId) { + return imageRepository.findImagesByProjectId(projectId) + .stream().map(ImageRequest::of).toList(); + } + + /** + * 프로젝트 타입 조회 + */ 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) { } } diff --git a/backend/src/main/java/com/worlabel/global/config/SecurityConfig.java b/backend/src/main/java/com/worlabel/global/config/SecurityConfig.java index 7082e7f..9c37c6d 100644 --- a/backend/src/main/java/com/worlabel/global/config/SecurityConfig.java +++ b/backend/src/main/java/com/worlabel/global/config/SecurityConfig.java @@ -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 diff --git a/backend/src/main/java/com/worlabel/global/config/WebConfig.java b/backend/src/main/java/com/worlabel/global/config/WebConfig.java index d727b71..7116fc6 100644 --- a/backend/src/main/java/com/worlabel/global/config/WebConfig.java +++ b/backend/src/main/java/com/worlabel/global/config/WebConfig.java @@ -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 resolvers) { resolvers.add(currentUserArgumentResolver); } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } From e38fe4229346486d41b5d2b19fff5e3df6ab2544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=88=98?= Date: Wed, 4 Sep 2024 15:38:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Feat:=20Auto=20=EB=A0=88=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EB=A7=81=20s3=20=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worlabel/domain/label/entity/Label.java | 8 +++++ .../label/repository/LabelRepository.java | 11 +++++++ .../domain/label/service/LabelService.java | 33 +++++++++++-------- .../worlabel/global/exception/ErrorCode.java | 4 +-- .../global/service/S3UploadService.java | 25 +++++++++++--- 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 backend/src/main/java/com/worlabel/domain/label/repository/LabelRepository.java diff --git a/backend/src/main/java/com/worlabel/domain/label/entity/Label.java b/backend/src/main/java/com/worlabel/domain/label/entity/Label.java index 2f63dfc..a4f7aff 100644 --- a/backend/src/main/java/com/worlabel/domain/label/entity/Label.java +++ b/backend/src/main/java/com/worlabel/domain/label/entity/Label.java @@ -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; + } + } diff --git a/backend/src/main/java/com/worlabel/domain/label/repository/LabelRepository.java b/backend/src/main/java/com/worlabel/domain/label/repository/LabelRepository.java new file mode 100644 index 0000000..1f84962 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/label/repository/LabelRepository.java @@ -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 { + + Optional