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:
commit
065ac8e334
@ -5,11 +5,14 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface FolderRepository extends JpaRepository<Folder, Integer> {
|
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);
|
boolean existsByIdAndProjectId(Integer folderId, Integer projectId);
|
||||||
}
|
}
|
@ -33,7 +33,7 @@ public class FolderService {
|
|||||||
|
|
||||||
Folder parent = null;
|
Folder parent = null;
|
||||||
if (folderRequest.getParentId() != 0) {
|
if (folderRequest.getParentId() != 0) {
|
||||||
parent = getFolder(folderRequest.getParentId());
|
parent = getFolder(folderRequest.getParentId(),projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Folder folder = Folder.of(folderRequest.getTitle(), parent, project);
|
Folder folder = Folder.of(folderRequest.getTitle(), parent, project);
|
||||||
@ -51,9 +51,9 @@ public class FolderService {
|
|||||||
|
|
||||||
// 최상위 폴더
|
// 최상위 폴더
|
||||||
if (folderId == 0) {
|
if (folderId == 0) {
|
||||||
return FolderResponse.from(folderRepository.findAllByParentIsNull());
|
return FolderResponse.from(folderRepository.findAllByProjectIdAndParentIsNull(projectId));
|
||||||
} else {
|
} 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) {
|
public FolderResponse updateFolder(final Integer memberId, final Integer projectId, final Integer folderId, final FolderRequest updatedFolderRequest) {
|
||||||
checkUnauthorized(memberId, projectId);
|
checkUnauthorized(memberId, projectId);
|
||||||
Folder folder = getFolder(folderId);
|
Folder folder = getFolder(folderId,projectId);
|
||||||
|
|
||||||
Folder parentFolder = folderRepository.findById(updatedFolderRequest.getParentId())
|
Folder parentFolder = folderRepository.findById(updatedFolderRequest.getParentId())
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
@ -77,7 +77,7 @@ public class FolderService {
|
|||||||
*/
|
*/
|
||||||
public void deleteFolder(final Integer memberId, final Integer projectId, final Integer folderId) {
|
public void deleteFolder(final Integer memberId, final Integer projectId, final Integer folderId) {
|
||||||
checkUnauthorized(memberId, projectId);
|
checkUnauthorized(memberId, projectId);
|
||||||
Folder folder = getFolder(folderId);
|
Folder folder = getFolder(folderId,projectId);
|
||||||
folderRepository.delete(folder);
|
folderRepository.delete(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,8 +86,8 @@ public class FolderService {
|
|||||||
.orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND));
|
.orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Folder getFolder(final Integer folderId) {
|
private Folder getFolder(final Integer folderId, final Integer projectId) {
|
||||||
return folderRepository.findById(folderId)
|
return folderRepository.findAllByProjectIdAndId(projectId,folderId)
|
||||||
.orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND));
|
.orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ public class FolderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkExistParticipant(final Integer memberId, final Integer projectId) {
|
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);
|
throw new CustomException(ErrorCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,20 @@ package com.worlabel.domain.image.repository;
|
|||||||
|
|
||||||
import com.worlabel.domain.image.entity.Image;
|
import com.worlabel.domain.image.entity.Image;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface ImageRepository extends JpaRepository<Image, Long> {
|
public interface ImageRepository extends JpaRepository<Image, Long> {
|
||||||
|
|
||||||
Optional<Image> findByIdAndFolderId(Long imageId, Integer folderId);
|
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);
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ public class ImageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkEditorParticipant(final Integer memberId, final Integer projectId) {
|
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);
|
throw new CustomException(ErrorCode.PARTICIPANT_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -41,4 +41,12 @@ public class Label extends BaseEntity {
|
|||||||
// @ManyToOne(fetch = FetchType.LAZY)
|
// @ManyToOne(fetch = FetchType.LAZY)
|
||||||
// @JoinColumn(name = "label_category_id")
|
// @JoinColumn(name = "label_category_id")
|
||||||
// private LabelCategory labelCategory;
|
// private LabelCategory labelCategory;
|
||||||
|
|
||||||
|
public static Label of(String jsonUrl, Image image) {
|
||||||
|
Label label = new Label();
|
||||||
|
label.url = jsonUrl;
|
||||||
|
label.image = image;
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
@ -119,4 +119,5 @@ public class ProjectController {
|
|||||||
projectService.removeProjectMember(memberId, projectId, removeMemberId);
|
projectService.removeProjectMember(memberId, projectId, removeMemberId);
|
||||||
return SuccessResponse.empty();
|
return SuccessResponse.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ public class ProjectResponse {
|
|||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Schema(description = "워크스페이스 id", example = "1")
|
@Schema(description = "워크스페이스 id", example = "1")
|
||||||
private Integer projectId;
|
private Integer workspaceId;
|
||||||
|
|
||||||
@Schema(description = "프로젝트 타입", example = "classification")
|
@Schema(description = "프로젝트 타입", example = "classification")
|
||||||
private ProjectType projectType;
|
private ProjectType projectType;
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package com.worlabel.domain.project.repository;
|
package com.worlabel.domain.project.repository;
|
||||||
|
|
||||||
import com.worlabel.domain.project.entity.Project;
|
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.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import javax.swing.text.html.Option;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ProjectRepository extends JpaRepository<Project, Integer> {
|
public interface ProjectRepository extends JpaRepository<Project, Integer> {
|
||||||
@ -24,4 +27,8 @@ public interface ProjectRepository extends JpaRepository<Project, Integer> {
|
|||||||
@Param("memberId") Integer memberId,
|
@Param("memberId") Integer memberId,
|
||||||
@Param("lastProjectId") Integer lastProjectId,
|
@Param("lastProjectId") Integer lastProjectId,
|
||||||
@Param("pageSize") Integer pageSize);
|
@Param("pageSize") Integer pageSize);
|
||||||
|
|
||||||
|
// ProjectType을 가져오는 메서드 추가
|
||||||
|
@Query("SELECT p.projectType FROM Project p WHERE p.id = :projectId")
|
||||||
|
Optional<ProjectType> findProjectTypeById(@Param("projectId") Integer projectId);
|
||||||
}
|
}
|
||||||
|
@ -17,15 +17,18 @@ import com.worlabel.domain.workspace.repository.WorkspaceRepository;
|
|||||||
import com.worlabel.global.exception.CustomException;
|
import com.worlabel.global.exception.CustomException;
|
||||||
import com.worlabel.global.exception.ErrorCode;
|
import com.worlabel.global.exception.ErrorCode;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class ProjectService {
|
public class ProjectService {
|
||||||
|
|
||||||
private final ProjectRepository projectRepository;
|
private final ProjectRepository projectRepository;
|
||||||
@ -34,6 +37,7 @@ public class ProjectService {
|
|||||||
private final MemberRepository memberRepository;
|
private final MemberRepository memberRepository;
|
||||||
private final WorkspaceParticipantRepository workspaceParticipantRepository;
|
private final WorkspaceParticipantRepository workspaceParticipantRepository;
|
||||||
|
|
||||||
|
|
||||||
public ProjectResponse createProject(final Integer memberId, final Integer workspaceId, final ProjectRequest projectRequest) {
|
public ProjectResponse createProject(final Integer memberId, final Integer workspaceId, final ProjectRequest projectRequest) {
|
||||||
Workspace workspace = getWorkspace(memberId, workspaceId);
|
Workspace workspace = getWorkspace(memberId, workspaceId);
|
||||||
Member member = getMember(memberId);
|
Member member = getMember(memberId);
|
||||||
@ -107,6 +111,7 @@ public class ProjectService {
|
|||||||
participantRepository.delete(participant);
|
participantRepository.delete(participant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Workspace getWorkspace(final Integer memberId, final Integer workspaceId) {
|
private Workspace getWorkspace(final Integer memberId, final Integer workspaceId) {
|
||||||
return workspaceRepository.findByMemberIdAndId(memberId, workspaceId)
|
return workspaceRepository.findByMemberIdAndId(memberId, workspaceId)
|
||||||
.orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND));
|
.orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND));
|
||||||
@ -146,3 +151,4 @@ public class ProjectService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
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.NoHandlerFoundException;
|
||||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||||
|
|
||||||
|
import java.lang.reflect.Executable;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -57,6 +59,13 @@ public class CustomControllerAdvice {
|
|||||||
return ErrorResponse.of(new CustomException(ErrorCode.EMPTY_REQUEST_PARAMETER));
|
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)
|
@ExceptionHandler(CustomException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e, HttpServletRequest request) {
|
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e, HttpServletRequest request) {
|
||||||
log.error("", e);
|
log.error("", e);
|
||||||
@ -65,9 +74,11 @@ public class CustomControllerAdvice {
|
|||||||
.body(ErrorResponse.of(e));
|
.body(ErrorResponse.of(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void sendNotification(Exception e, HttpServletRequest request) {
|
private void sendNotification(Exception e, HttpServletRequest request) {
|
||||||
// TODO: 추후 주석 해제
|
// TODO: 추후 주석 해제
|
||||||
notificationManager.sendNotification(e, request.getRequestURI(),getParams(request));
|
// notificationManager.sendNotification(e, request.getRequestURI(),getParams(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getParams(HttpServletRequest req) {
|
private String getParams(HttpServletRequest req) {
|
||||||
|
@ -64,8 +64,11 @@ public class SecurityConfig {
|
|||||||
// 경로별 인가 작업
|
// 경로별 인가 작업
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests(auth->auth
|
.authorizeHttpRequests(auth->auth
|
||||||
|
.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**").permitAll()
|
||||||
.requestMatchers("/api/auth/reissue").permitAll()
|
.requestMatchers("/api/auth/reissue").permitAll()
|
||||||
.anyRequest().authenticated());
|
.anyRequest().authenticated()
|
||||||
|
// .anyRequest().permitAll()
|
||||||
|
);
|
||||||
|
|
||||||
// OAuth2
|
// OAuth2
|
||||||
http
|
http
|
||||||
|
@ -2,7 +2,9 @@ package com.worlabel.global.config;
|
|||||||
|
|
||||||
import com.worlabel.global.resolver.CurrentUserArgumentResolver;
|
import com.worlabel.global.resolver.CurrentUserArgumentResolver;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@ -18,4 +20,9 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||||
resolvers.add(currentUserArgumentResolver);
|
resolvers.add(currentUserArgumentResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ public enum ErrorCode {
|
|||||||
INVALID_URL(HttpStatus.BAD_REQUEST, 1004, "제공하지 않는 주소입니다. 확인해주세요"),
|
INVALID_URL(HttpStatus.BAD_REQUEST, 1004, "제공하지 않는 주소입니다. 확인해주세요"),
|
||||||
FAIL_TO_CREATE_FILE(HttpStatus.BAD_REQUEST, 1005, "파일 업로드에 실패하였습니다. 다시 한번 확인해주세요"),
|
FAIL_TO_CREATE_FILE(HttpStatus.BAD_REQUEST, 1005, "파일 업로드에 실패하였습니다. 다시 한번 확인해주세요"),
|
||||||
FAIL_TO_DELETE_FILE(HttpStatus.BAD_REQUEST, 1006, "파일 삭제에 실패하였습니다. 다시 한번 확인해주세요"),
|
FAIL_TO_DELETE_FILE(HttpStatus.BAD_REQUEST, 1006, "파일 삭제에 실패하였습니다. 다시 한번 확인해주세요"),
|
||||||
|
INVALID_FILE_PATH(HttpStatus.BAD_REQUEST,1007 , "파일 경로가 잘못되었습니다. 다시 한번 확인해주세요"),
|
||||||
|
|
||||||
// Auth & Member - 2000
|
// Auth & Member - 2000
|
||||||
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
|
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
|
||||||
@ -47,8 +48,10 @@ public enum ErrorCode {
|
|||||||
// Image - 7000
|
// Image - 7000
|
||||||
IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, 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 HttpStatus status;
|
||||||
private final int code;
|
private final int code;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@ -22,6 +23,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
// TODO: 추후 비동기로 변경해야합니다.
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -44,6 +46,32 @@ public class S3UploadService {
|
|||||||
@Value("${cloud.aws.url}")
|
@Value("${cloud.aws.url}")
|
||||||
private String 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) {
|
private String uploadImage(final MultipartFile image, final Integer projectId) {
|
||||||
try {
|
try {
|
||||||
return uploadToS3(image, projectId);
|
return uploadImageToS3(image, projectId);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new CustomException(ErrorCode.FAIL_TO_CREATE_FILE);
|
throw new CustomException(ErrorCode.FAIL_TO_CREATE_FILE);
|
||||||
}
|
}
|
||||||
@ -69,7 +97,7 @@ public class S3UploadService {
|
|||||||
/**
|
/**
|
||||||
* AWS S3 이미지 업로드
|
* 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 originalFileName = image.getOriginalFilename(); // 원본 파일 이름
|
||||||
String extension = originalFileName.substring(originalFileName.lastIndexOf(".") + 1); // 파일 확장자
|
String extension = originalFileName.substring(originalFileName.lastIndexOf(".") + 1); // 파일 확장자
|
||||||
|
|
||||||
@ -99,11 +127,11 @@ public class S3UploadService {
|
|||||||
return url.getPath();
|
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;
|
return projectId + "/" + UUID.randomUUID().toString().substring(0, 13) + "." + extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteImageFromS3(String imageAddress){
|
public void deleteImageFromS3(String imageAddress) {
|
||||||
String key = getKeyFromImageAddress(imageAddress);
|
String key = getKeyFromImageAddress(imageAddress);
|
||||||
try {
|
try {
|
||||||
amazonS3.deleteObject(new DeleteObjectRequest(bucket, key));
|
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) {
|
private String getKeyFromImageAddress(String imageAddress) {
|
||||||
|
if (!StringUtils.hasText(imageAddress)) {
|
||||||
|
throw new CustomException(ErrorCode.INVALID_FILE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
URL url = new URL(imageAddress);
|
URL url = new URL(imageAddress);
|
||||||
String decodingKey = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
|
String decodingKey = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
|
||||||
return decodingKey.substring(1);
|
return decodingKey.substring(1);
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new CustomException(ErrorCode.FAIL_TO_DELETE_FILE);
|
throw new CustomException(ErrorCode.INVALID_FILE_PATH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user