diff --git a/build.gradle b/build.gradle index a728d3f..1f7e73e 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,17 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //이미지 저장을 위한 s3의존성 + implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.529') + implementation 'com.amazonaws:aws-java-sdk-s3' + + //ocr을 위한 goolge vision 의존성 + implementation 'com.google.cloud:google-cloud-vision:3.44.0' + implementation 'com.google.cloud:google-cloud-storage:2.10.0' + + //health check를 위한 의존성 + implementation("org.springframework.boot:spring-boot-starter-actuator") } tasks.named('test') { diff --git a/src/main/java/com/easylead/easylead/common/error/ErrorCode.java b/src/main/java/com/easylead/easylead/common/error/ErrorCode.java index a3c2824..c60c263 100644 --- a/src/main/java/com/easylead/easylead/common/error/ErrorCode.java +++ b/src/main/java/com/easylead/easylead/common/error/ErrorCode.java @@ -7,7 +7,16 @@ @AllArgsConstructor @Getter public enum ErrorCode { - SERVER_ERROR("G500",HttpStatus.INTERNAL_SERVER_ERROR, "요청 수행 중 서버 에러 발생"); + + SERVER_ERROR("G500",HttpStatus.INTERNAL_SERVER_ERROR, "요청 수행 중 서버 에러 발생"), + // S3 관련 에러 코드 + EMPTY_FILE_EXCEPTION("F500-1", HttpStatus.BAD_REQUEST, "빈 파일입니다."), + NO_FILE_EXTENTION("F500-2", HttpStatus.BAD_REQUEST, "확장자가 없습니다."), + INVALID_FILE_EXTENTION("F500-3", HttpStatus.BAD_REQUEST, "부적절한 확장자입니다."), + IO_EXCEPTION_ON_IMAGE_UPLOAD("F502-1", HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드 중 에러가 발생했습니다."), + IO_EXCEPTION_ON_FILE_UPLOAD("F502-1", HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드 중 에러가 발생했습니다."), + PUT_OBJECT_EXCEPTION("F502-2", HttpStatus.INTERNAL_SERVER_ERROR, "S3에 이미지 업로드 중 에러가 발생했습니다."), + IO_EXCEPTION_ON_IMAGE_DELETE("F502-3", HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제 중 에러가 발생했습니다."); private final String errorCode; private final HttpStatus httpStatusCode; diff --git a/src/main/java/com/easylead/easylead/config/S3Config.java b/src/main/java/com/easylead/easylead/config/S3Config.java new file mode 100644 index 0000000..5e842e3 --- /dev/null +++ b/src/main/java/com/easylead/easylead/config/S3Config.java @@ -0,0 +1,28 @@ +package com.easylead.easylead.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials= new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/com/easylead/easylead/domain/books/entity/Book.java b/src/main/java/com/easylead/easylead/domain/books/entity/Book.java index 49605bb..0f8aff9 100644 --- a/src/main/java/com/easylead/easylead/domain/books/entity/Book.java +++ b/src/main/java/com/easylead/easylead/domain/books/entity/Book.java @@ -1,21 +1,22 @@ package com.easylead.easylead.domain.books.entity; -import com.easylead.easylead.common.entity.BaseEntity; import jakarta.persistence.Entity; +import jakarta.persistence.Id; import lombok.*; -import lombok.experimental.SuperBuilder; import java.util.Date; import static lombok.AccessLevel.PROTECTED; @NoArgsConstructor(access = PROTECTED) -@SuperBuilder +@Builder @AllArgsConstructor @EqualsAndHashCode(callSuper = false) @Entity @Getter -public class Book extends BaseEntity { +public class Book { + @Id + private String ISBN; private String title; private String writer; private String publisher; diff --git a/src/main/java/com/easylead/easylead/domain/content/entity/Content.java b/src/main/java/com/easylead/easylead/domain/content/entity/Content.java new file mode 100644 index 0000000..7922055 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/content/entity/Content.java @@ -0,0 +1,26 @@ +package com.easylead.easylead.domain.content.entity; + +import com.easylead.easylead.domain.books.entity.Book; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; + +@NoArgsConstructor() +@AllArgsConstructor +@Entity +@Getter +public class Content { + @EmbeddedId + private ContentId contentId; + + @ManyToOne(fetch = LAZY) + @MapsId("ISBN") + @JoinColumn(name = "ISBN") + private Book book; + + private String pageContent; + private String pageImg; +} diff --git a/src/main/java/com/easylead/easylead/domain/content/entity/ContentId.java b/src/main/java/com/easylead/easylead/domain/content/entity/ContentId.java new file mode 100644 index 0000000..0fb1fbe --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/content/entity/ContentId.java @@ -0,0 +1,19 @@ +package com.easylead.easylead.domain.content.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Embeddable +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ContentId implements Serializable { + private Long pageId; + private String ISBN; +} diff --git a/src/main/java/com/easylead/easylead/domain/gpt/service/GptService.java b/src/main/java/com/easylead/easylead/domain/gpt/service/GptService.java index 59e7115..853e13f 100644 --- a/src/main/java/com/easylead/easylead/domain/gpt/service/GptService.java +++ b/src/main/java/com/easylead/easylead/domain/gpt/service/GptService.java @@ -205,6 +205,22 @@ public Flux askCustomStream(String text) throws JsonProcessingException List messages = new ArrayList<>(); + // 시스템 역할 설정 + messages.add(new Message("\"너는 입력받은 한국어를 쉬운 한국어로 변환해주는 도우미야. " + + "다음 조건들을 모두 충족하는 내용으로 변환해서 알려줘. \\\\n " + + "1. 입력받은 한국어 문장을 이해하기 쉬운 한국어 문장으로 변환해줘. " + + "2. 문장은 간결하게, 한 문장이 길어지면 두 문장으로 나눠서 변환해줘. " + + "3. 꾸미는 말 빼고, 이어진 문장은 두 개의 문장으로 변환해줘. " + + "4. 주어를 중심으로 알기 쉽게 변환해줘. " + + "5. 최대한 능동형 문장으로, 서술식의 구어체(-합니다, -입니다)로 변환해줘. " + + "6. 추상적 표현과 비유는 자제하도록 해. " + + "7. 이중부정 문장은 이해하기 쉬운 문장으로 바꿔. " + + "8. 한 문장에 한 줄씩 적어야해. " + + "9. 대화문은 문장 전후로 한 줄 띄어줘. " + + "10. 단어는 일상생활에서 자주 쓰는, 가능한 짧고 이해하기 쉬운 단어로 사용하도록 해. " + + "11. 한자어나 외국어를 풀어서 쉬운 말로 변환해. " + + "12. 약어가 있으면 다음 문장에 설명을 추가해.", "system")); + messages.add(new Message(text,"user")); ChatGPTRequestDTO chatGptRequest = new ChatGPTRequestDTO( @@ -228,9 +244,24 @@ public HttpRequest requestGPTCustom(String text) throws JsonProcessingException List messages = new ArrayList<>(); // Assistant API 사용할지 Prompt를 변경할지 선택하기 // 시스템 역할 설정 + messages.add(new Message("\"너는 입력받은 한국어를 쉬운 한국어로 변환해주는 도우미야. " + + "다음 조건들을 모두 충족하는 내용으로 변환해서 알려줘. \\\\n " + + "1. 입력받은 한국어 문장을 이해하기 쉬운 한국어 문장으로 변환해줘. " + + "2. 문장은 간결하게, 한 문장이 길어지면 두 문장으로 나눠서 변환해줘. " + + "3. 꾸미는 말 빼고, 이어진 문장은 두 개의 문장으로 변환해줘. " + + "4. 주어를 중심으로 알기 쉽게 변환해줘. " + + "5. 최대한 능동형 문장으로, 서술식의 구어체(-합니다, -입니다)로 변환해줘. " + + "6. 추상적 표현과 비유는 자제하도록 해. " + + "7. 이중부정 문장은 이해하기 쉬운 문장으로 바꿔. " + + "8. 한 문장에 한 줄씩 적어야해. " + + "9. 대화문은 문장 전후로 한 줄 띄어줘. " + + "10. 단어는 일상생활에서 자주 쓰는, 가능한 짧고 이해하기 쉬운 단어로 사용하도록 해. " + + "11. 한자어나 외국어를 풀어서 쉬운 말로 변환해. " + + "12. 약어가 있으면 다음 문장에 설명을 추가해.", "system")); + messages.add(new Message(text, "user")); - ChatGPTRequestDTO chatGptRequest = new ChatGPTRequestDTO("ft:gpt-3.5-turbo-0125:personal::9ldfWO0p", messages, 0.3,false); + ChatGPTRequestDTO chatGptRequest = new ChatGPTRequestDTO("ft:gpt-3.5-turbo-0613:personal::9prSIgJ8", messages, 0.3,false); String input = null; input = mapper.writeValueAsString(chatGptRequest); System.out.println(input); @@ -295,4 +326,30 @@ public String responseDalle(HttpRequest request) throws JsonProcessingException } + + public HttpRequest requestImgPrompt(String reqText) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + List messages = new ArrayList<>(); + // Assistant API 사용할지 Prompt를 변경할지 선택하기 + // 시스템 역할 설정 + messages.add(new Message("너는 동화책 삽화에 대해 잘알고, 내용에 중요한 부분을 삽화 프롬프트로 작성할 수 있는 전문가야. ", "system")); + + messages.add(new Message(reqText+"\n\n 이 내용을 토대로 동화책 삽화 1개만 그리고 싶어. 따뜻한 느낌의 동화책에 맞는 그림체로 삽화 만드는 프롬프트 작성해줘 ", "user")); + + ChatGPTRequestDTO chatGptRequest = new ChatGPTRequestDTO("gpt-4", messages, 0.3,false); + String input = null; + input = mapper.writeValueAsString(chatGptRequest); + System.out.println(input); + System.out.println("apikey : " + gptApiKey); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.openai.com/v1/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + gptApiKey) + .POST(HttpRequest.BodyPublishers.ofString(input)) + .build(); + + return request; + } + } diff --git a/src/main/java/com/easylead/easylead/domain/read/entity/Read.java b/src/main/java/com/easylead/easylead/domain/read/entity/Read.java new file mode 100644 index 0000000..40a51be --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/read/entity/Read.java @@ -0,0 +1,43 @@ +package com.easylead.easylead.domain.read.entity; + +import com.easylead.easylead.domain.books.entity.Book; +import com.easylead.easylead.domain.users.entity.Users; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +@Builder +@AllArgsConstructor +@EqualsAndHashCode +@NoArgsConstructor(access = PROTECTED) +@Getter +@Entity +public class Read { + @EmbeddedId + private ReadId readId; + + @ManyToOne(fetch = LAZY) + @MapsId("userId") + @JoinColumn(name = "read_user_id") + private Users readUser; + + @ManyToOne(fetch = LAZY) + @MapsId("ISBN") + @JoinColumn(name = "ISBN") + private Book book; + + private Long page; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updateAt; +} diff --git a/src/main/java/com/easylead/easylead/domain/read/entity/ReadId.java b/src/main/java/com/easylead/easylead/domain/read/entity/ReadId.java new file mode 100644 index 0000000..b4cd891 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/read/entity/ReadId.java @@ -0,0 +1,19 @@ +package com.easylead.easylead.domain.read.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Embeddable +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ReadId implements Serializable { + private Long userId; + private String ISBN; +} diff --git a/src/main/java/com/easylead/easylead/domain/request/entity/Progress.java b/src/main/java/com/easylead/easylead/domain/request/entity/Progress.java new file mode 100644 index 0000000..26d5902 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/request/entity/Progress.java @@ -0,0 +1,16 @@ +package com.easylead.easylead.domain.request.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Progress { + P0("접수 완료"), + P1("담당자 확인 중"), + P2("글맞춤 중"), + P3("검수 중"), + P4("글맞춤 완료"); + + private final String description; +} diff --git a/src/main/java/com/easylead/easylead/domain/request/entity/Request.java b/src/main/java/com/easylead/easylead/domain/request/entity/Request.java new file mode 100644 index 0000000..d5ff9b1 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/request/entity/Request.java @@ -0,0 +1,46 @@ +package com.easylead.easylead.domain.request.entity; + +import com.easylead.easylead.domain.books.entity.Book; +import com.easylead.easylead.domain.users.entity.Users; +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +@Builder +@AllArgsConstructor +@EqualsAndHashCode +@NoArgsConstructor(access = PROTECTED) +@Getter +@Entity +public class Request { + @EmbeddedId + private RequestId requestId; + + @ManyToOne(fetch = LAZY) + @MapsId("userId") + @JoinColumn(name = "user_id") + private Users readUser; + + @ManyToOne(fetch = LAZY) + @MapsId("ISBN") + @JoinColumn(name = "ISBN") + private Book book; + + @Enumerated(EnumType.STRING) + private Progress progress; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updateAt; + +} diff --git a/src/main/java/com/easylead/easylead/domain/request/entity/RequestId.java b/src/main/java/com/easylead/easylead/domain/request/entity/RequestId.java new file mode 100644 index 0000000..e22fa1f --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/request/entity/RequestId.java @@ -0,0 +1,19 @@ +package com.easylead.easylead.domain.request.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Embeddable +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RequestId implements Serializable { + private Long userId; + private String ISBN; +} diff --git a/src/main/java/com/easylead/easylead/domain/text/business/TextBusiness.java b/src/main/java/com/easylead/easylead/domain/text/business/TextBusiness.java new file mode 100644 index 0000000..ff674ed --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/business/TextBusiness.java @@ -0,0 +1,63 @@ +package com.easylead.easylead.domain.text.business; + +import com.easylead.easylead.common.annotation.Business; +import com.easylead.easylead.common.error.ErrorCode; +import com.easylead.easylead.common.exception.ApiException; +import com.easylead.easylead.domain.gpt.service.GptService; +import com.easylead.easylead.domain.text.converter.TextConverter; +import com.easylead.easylead.domain.text.dto.TextFileResDTO; +import com.easylead.easylead.domain.text.service.GoogleVisionService; +import com.easylead.easylead.domain.text.service.S3Service; +import com.easylead.easylead.domain.text.service.TextService; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; +import reactor.core.publisher.Flux; + +import java.net.http.HttpRequest; +import java.util.List; +import java.util.Objects; + +@Business +@RequiredArgsConstructor +@Slf4j +public class TextBusiness { + + private final TextService textService; + private final GptService gptService; + private final S3Service s3Service; + private final GoogleVisionService googleVisionService; + private final TextConverter textConverter; + + + public TextFileResDTO easyToRead(MultipartFile file) throws Exception { + if (file.isEmpty() || Objects.isNull(file.getOriginalFilename())) { + throw new ApiException(ErrorCode.EMPTY_FILE_EXCEPTION); + } + + List reqText = textService.detectTextPDF(file); + + log.info("=========== reqText : "+reqText+"============"); + + StringBuilder result = new StringBuilder(); + for(String text : reqText){ + HttpRequest request = gptService.requestGPTCustom(text); + result.append(gptService.responseGPT(request)); + } + return textConverter.toTextFileResDTO(result.toString()); + + } + public Flux easyToReadImage(MultipartFile file) throws JsonProcessingException { + if (file.isEmpty() || Objects.isNull(file.getOriginalFilename())) { + throw new ApiException(ErrorCode.EMPTY_FILE_EXCEPTION); + } + + String reqText = textService.detectTextImage(file); + + log.info("=========== reqText : "+reqText+"============"); + + + return gptService.askCustomStream(reqText); + } +} diff --git a/src/main/java/com/easylead/easylead/domain/text/controller/TextController.java b/src/main/java/com/easylead/easylead/domain/text/controller/TextController.java new file mode 100644 index 0000000..5d7d805 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/controller/TextController.java @@ -0,0 +1,45 @@ +package com.easylead.easylead.domain.text.controller; + +import com.easylead.easylead.domain.text.business.TextBusiness; +import com.easylead.easylead.domain.text.dto.TextFileResDTO; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import reactor.core.publisher.Flux; + +import java.util.Locale; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/text") +@CrossOrigin(origins = "*", allowedHeaders = "*") +public class TextController { + private final TextBusiness textBusiness; + + @PostMapping(value = "/image", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux easyToReadImage(Locale locale, + HttpServletRequest request, + HttpServletResponse response, + @RequestPart(value = "image") MultipartFile file) throws JsonProcessingException { + try{ + return textBusiness.easyToReadImage(file); + + }catch (JsonProcessingException je){ + log.error(je.getMessage()); + return Flux.empty(); + } + } + + @PostMapping("/file") + public ResponseEntity easyToReadFile(@RequestPart(value = "file") MultipartFile file) throws Exception { + return ResponseEntity.ok(textBusiness.easyToRead(file)); + } + +} diff --git a/src/main/java/com/easylead/easylead/domain/text/converter/TextConverter.java b/src/main/java/com/easylead/easylead/domain/text/converter/TextConverter.java new file mode 100644 index 0000000..71a85ce --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/converter/TextConverter.java @@ -0,0 +1,15 @@ +package com.easylead.easylead.domain.text.converter; + +import com.easylead.easylead.common.annotation.Converter; +import com.easylead.easylead.domain.text.dto.TextFileResDTO; +import lombok.RequiredArgsConstructor; + +@Converter +@RequiredArgsConstructor +public class TextConverter { + public TextFileResDTO toTextFileResDTO(String resText) { + return TextFileResDTO.builder() + .text(resText) + .build(); + } +} diff --git a/src/main/java/com/easylead/easylead/domain/text/dto/TextFileResDTO.java b/src/main/java/com/easylead/easylead/domain/text/dto/TextFileResDTO.java new file mode 100644 index 0000000..456db9c --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/dto/TextFileResDTO.java @@ -0,0 +1,12 @@ +package com.easylead.easylead.domain.text.dto; + +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class TextFileResDTO { + String text; +} diff --git a/src/main/java/com/easylead/easylead/domain/text/service/GoogleStorageService.java b/src/main/java/com/easylead/easylead/domain/text/service/GoogleStorageService.java new file mode 100644 index 0000000..ea85580 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/service/GoogleStorageService.java @@ -0,0 +1,47 @@ +package com.easylead.easylead.domain.text.service; + +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +@Service +public class GoogleStorageService { + private final Storage storage = StorageOptions.getDefaultInstance().getService(); + private final String bucketName = "geulmatchum-file"; + + public Blob uploadFile(MultipartFile file) { + String fileName = file.getOriginalFilename(); + Blob blob = null; + try { + blob = storage.create( + Blob.newBuilder(bucketName, fileName).build(), + file.getBytes() + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + return blob; // Return the file's public URL + } + + public String getGcsSourcePath(MultipartFile file) { + // Upload the file and get its public URL + Blob blob = uploadFile(file); + // Convert the public URL to gcsSourcePath + return "gs://" + blob.getBucket() + "/" + blob.getName(); + } + + public String getGcsDestinationPath(String fileName) { + // Define the GCS path for saving results + // For example, saving results to a directory named "results" + return String.format("gs://%s/results/%s", bucketName, fileName); + } + +} diff --git a/src/main/java/com/easylead/easylead/domain/text/service/GoogleVisionService.java b/src/main/java/com/easylead/easylead/domain/text/service/GoogleVisionService.java new file mode 100644 index 0000000..21197a3 --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/service/GoogleVisionService.java @@ -0,0 +1,178 @@ +package com.easylead.easylead.domain.text.service; + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.google.cloud.vision.v1.*; +import com.google.protobuf.util.JsonFormat; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +@RequiredArgsConstructor +@Component +public class GoogleVisionService { + public String detechStringImage(String url){ + StopWatch totalTime = new StopWatch(); + totalTime.start(); + + List requests = new ArrayList<>(); + + ImageSource imgSource = ImageSource.newBuilder().setImageUri(url).build(); + Image img = Image.newBuilder().setSource(imgSource).build(); + Feature feat = Feature.newBuilder().setType(Feature.Type.TEXT_DETECTION).build(); + AnnotateImageRequest request = + AnnotateImageRequest.newBuilder().addFeatures(feat).setImage(img).build(); + requests.add(request); + + try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) { + BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests); + List responses = response.getResponsesList(); + + StringBuilder result = new StringBuilder(); + for (AnnotateImageResponse res : responses) { + if (res.hasError()) { + System.out.format("Error: %s%n", res.getError().getMessage()); + return null; + } + + for (EntityAnnotation annotation : res.getTextAnnotationsList()) { + result.append(annotation.getDescription()).append(" "); + } + } + + totalTime.stop(); + System.out.println("Total Time : " + totalTime.getTotalTimeMillis() + "ms"); + + return result.toString(); + } + catch (Exception exception) { + return exception.getMessage(); + } + } + public List detectDocumentsGcs(String gcsSourcePath, String gcsDestinationPath) + throws Exception { + List reqList = new ArrayList<>(); + StringBuilder fullText = new StringBuilder(); + + + AnnotateImageResponse annotateImageResponse = null; + try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) { + List requests = new ArrayList<>(); + + GcsSource gcsSource = GcsSource.newBuilder().setUri(gcsSourcePath).build(); + + InputConfig inputConfig = + InputConfig.newBuilder() + .setMimeType( + "application/pdf") // Supported MimeTypes: "application/pdf", "image/tiff" + .setGcsSource(gcsSource) + .build(); + + GcsDestination gcsDestination = + GcsDestination.newBuilder().setUri(gcsDestinationPath).build(); + + OutputConfig outputConfig = + OutputConfig.newBuilder().setBatchSize(2).setGcsDestination(gcsDestination).build(); + + // Select the Feature required by the vision API + Feature feature = Feature.newBuilder().setType(Feature.Type.DOCUMENT_TEXT_DETECTION).build(); + + // Build the OCR request + AsyncAnnotateFileRequest request = + AsyncAnnotateFileRequest.newBuilder() + .addFeatures(feature) + .setInputConfig(inputConfig) + .setOutputConfig(outputConfig) + .build(); + + requests.add(request); + + // Perform the OCR request + OperationFuture response = + client.asyncBatchAnnotateFilesAsync(requests); + + System.out.println("Waiting for the operation to finish."); + + // Wait for the request to finish. (The result is not used, since the API saves the result to + // the specified location on GCS.) + List result = + response.get(180, TimeUnit.SECONDS).getResponsesList(); + + // Once the request has completed and the System.output has been + // written to GCS, we can list all the System.output files. + Storage storage = StorageOptions.getDefaultInstance().getService(); + + // Get the destination location from the gcsDestinationPath + Pattern pattern = Pattern.compile("gs://([^/]+)/(.+)"); + Matcher matcher = pattern.matcher(gcsDestinationPath); + + if (matcher.find()) { + String bucketName = matcher.group(1); + String prefix = matcher.group(2); + + // Get the list of objects with the given prefix from the GCS bucket + Bucket bucket = storage.get(bucketName); + com.google.api.gax.paging.Page pageList = bucket.list(Storage.BlobListOption.prefix(prefix)); + + Blob firstOutputFile = null; + + // List objects with the given prefix. + System.out.println("Output files:"); + for (Blob blob : pageList.iterateAll()) { + System.out.println(blob.getName()); + + String jsonContents = new String(blob.getContent()); + AnnotateFileResponse.Builder builder = AnnotateFileResponse.newBuilder(); + JsonFormat.parser().merge(jsonContents, builder); + + AnnotateFileResponse annotateFileResponse = builder.build(); + for (AnnotateImageResponse a : annotateFileResponse.getResponsesList()) { + reqList.add(a.getFullTextAnnotation().getText()); + } + // Process the first System.output file from GCS. + // Since we specified batch size = 2, the first response contains + // the first two pages of the input file. +// if (firstOutputFile == null) { +// firstOutputFile = blob; +// } + } + + // Get the contents of the file and convert the JSON contents to an AnnotateFileResponse + // object. If the Blob is small read all its content in one request + // (Note: the file is a .json file) + // Storage guide: https://cloud.google.com/storage/docs/downloading-objects +// String jsonContents = new String(firstOutputFile.getContent()); +// AnnotateFileResponse.Builder builder = AnnotateFileResponse.newBuilder(); +// JsonFormat.parser().merge(jsonContents, builder); +// +// // Build the AnnotateFileResponse object +// AnnotateFileResponse annotateFileResponse = builder.build(); + + // Parse through the object to get the actual response for the first page of the input file. +// annotateImageResponse = annotateFileResponse.getResponses(0); + + // Here we print the full text from the first page. + // The response contains more information: + // annotation/pages/blocks/paragraphs/words/symbols + // including confidence score and bounding boxes + + } else { + System.out.println("No MATCH"); + } + } + return reqList; + } + +} diff --git a/src/main/java/com/easylead/easylead/domain/text/service/S3Service.java b/src/main/java/com/easylead/easylead/domain/text/service/S3Service.java new file mode 100644 index 0000000..3ac546c --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/service/S3Service.java @@ -0,0 +1,120 @@ +package com.easylead.easylead.domain.text.service; + + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.util.IOUtils; +import com.easylead.easylead.common.error.ErrorCode; +import com.easylead.easylead.common.exception.ApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +@Component +public class S3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucketName}") + private String bucketName; + + + public String uploadImage(MultipartFile image) { + this.validateImageFileExtention(image.getOriginalFilename()); + try { + return this.uploadImageToS3(image); + } catch (IOException e) { + throw new ApiException(ErrorCode.IO_EXCEPTION_ON_IMAGE_UPLOAD); + } + } + + public String uploadPDF(MultipartFile file) { + this.validatePDFExtention(file.getOriginalFilename()); + try { + return this.uploadImageToS3(file); + } catch (IOException e) { + throw new ApiException(ErrorCode.IO_EXCEPTION_ON_FILE_UPLOAD); + } + } + + /** + * 이미지 파일의 확장자 명이 올바른지 확인 + */ + private void validateImageFileExtention(String filename) { + int lastDotIndex = filename.lastIndexOf("."); + if (lastDotIndex == -1) { + throw new ApiException(ErrorCode.NO_FILE_EXTENTION); + } + + String extension = filename.substring(lastDotIndex + 1).toLowerCase(); + List allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif"); + + if (!allowedExtentionList.contains(extension)) { + throw new ApiException(ErrorCode.INVALID_FILE_EXTENTION); + } + } + + /** + * PDF 파일의 확장자 명이 올바른지 확인 + */ + private void validatePDFExtention(String filename) { + int lastDotIndex = filename.lastIndexOf("."); + if (lastDotIndex == -1) { + throw new ApiException(ErrorCode.NO_FILE_EXTENTION); + } + + String extension = filename.substring(lastDotIndex + 1).toLowerCase(); + if (!extension.equals("pdf")) { + throw new ApiException(ErrorCode.INVALID_FILE_EXTENTION); + } + } + + /** + * 실제 S3에 이미지 업로드하는 메서드 + */ + private String uploadImageToS3(MultipartFile image) throws IOException { + String originalFilename = image.getOriginalFilename(); //원본 파일 명 + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명 + + String s3FileName = + UUID.randomUUID().toString().substring(0, 10) + originalFilename; //실제 S3에 저장될 파일 명 + + InputStream is = image.getInputStream(); + byte[] bytes = IOUtils.toByteArray(is); // image를 byte 배열로 변환 + + ObjectMetadata metadata = new ObjectMetadata(); // metadata 생성 + metadata.setContentType("image/" + extension); + metadata.setContentLength(bytes.length); + + // S3에 요청할 때 사용할 byteInputStream 생성 + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + + try { + PutObjectRequest putObjectRequest = + new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata); + + // 실제 S3에 이미지 업로드하는 코드 + amazonS3.putObject(putObjectRequest); + } catch (Exception e) { + throw new ApiException(ErrorCode.PUT_OBJECT_EXCEPTION, e); + } finally { + byteArrayInputStream.close(); + is.close(); + } + + return amazonS3.getUrl(bucketName, s3FileName).toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/easylead/easylead/domain/text/service/TextService.java b/src/main/java/com/easylead/easylead/domain/text/service/TextService.java new file mode 100644 index 0000000..5017a4a --- /dev/null +++ b/src/main/java/com/easylead/easylead/domain/text/service/TextService.java @@ -0,0 +1,33 @@ +package com.easylead.easylead.domain.text.service; + +import com.easylead.easylead.common.error.ErrorCode; +import com.easylead.easylead.common.exception.ApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class TextService { + private final S3Service s3Service; + private final GoogleVisionService googleVisionService; + private final GoogleStorageService googleStorageService; + + public List detectTextPDF(MultipartFile file) throws Exception { + String gcsSourcePath = googleStorageService.getGcsSourcePath(file); + log.info("=========gcsSourcePath : "+gcsSourcePath+"=============="); + String gcsDestinationPath = googleStorageService.getGcsDestinationPath(file.getOriginalFilename()); + List reqtext = googleVisionService.detectDocumentsGcs(gcsSourcePath,gcsDestinationPath); + + return reqtext; + } + + public String detectTextImage(MultipartFile file) { + String imgUrl = s3Service.uploadImage(file); + return googleVisionService.detechStringImage(imgUrl); + } +} diff --git a/src/main/java/com/easylead/easylead/domain/users/entity/Users.java b/src/main/java/com/easylead/easylead/domain/users/entity/Users.java index 7eb84a9..04310e6 100644 --- a/src/main/java/com/easylead/easylead/domain/users/entity/Users.java +++ b/src/main/java/com/easylead/easylead/domain/users/entity/Users.java @@ -1,19 +1,20 @@ package com.easylead.easylead.domain.users.entity; -import com.easylead.easylead.common.entity.BaseEntity; import jakarta.persistence.Entity; +import jakarta.persistence.Id; import lombok.*; -import lombok.experimental.SuperBuilder; import static lombok.AccessLevel.PROTECTED; @NoArgsConstructor(access = PROTECTED) -@SuperBuilder +@Builder @AllArgsConstructor @EqualsAndHashCode(callSuper = false) @Entity @Getter -public class Users extends BaseEntity { +public class Users { + @Id + private Long userId; private String name; private String email; private String password; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 460f85d..cacfacb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,5 @@ spring.application.name=easy-lead -spring.config.import=application-key.properties \ No newline at end of file +spring.config.import=application-key.properties +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always +spring.servlet.multipart.max-file-size=10GB \ No newline at end of file