diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6442c2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM gradle:jdk21-jammy + +WORKDIR /app + +CMD ["gradle", "bootRun"] \ No newline at end of file diff --git a/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java b/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java index 4688f41..8b171bb 100644 --- a/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java +++ b/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java @@ -16,7 +16,7 @@ public class KakaoOauthClient implements OauthClient { @Override public Member fetchMember(String accessToken) { - return kakaoClient.fetchMember(accessToken).toDomain(); + return kakaoClient.fetchMember("Bearer " + accessToken).toDomain(); } @Override diff --git a/src/main/java/notai/common/config/AuthInterceptor.java b/src/main/java/notai/common/config/AuthInterceptor.java index 87c168c..77d3a64 100644 --- a/src/main/java/notai/common/config/AuthInterceptor.java +++ b/src/main/java/notai/common/config/AuthInterceptor.java @@ -3,9 +3,12 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import notai.auth.TokenService; +import notai.common.exception.ErrorMessages; +import notai.common.exception.type.UnAuthorizedException; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +import static notai.common.exception.ErrorMessages.*; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @Component @@ -22,14 +25,16 @@ public AuthInterceptor(TokenService tokenService) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String header = request.getHeader(AUTHORIZATION); if (header == null || !header.startsWith(AUTHENTICATION_TYPE)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; + throw new UnAuthorizedException(NOTFOUND_ACCESS_TOKEN); } String token = header.substring(BEARER_PREFIX_LENGTH); - Long memberId = tokenService.extractMemberId(token); - request.setAttribute("memberId", memberId); - + try { + Long memberId = tokenService.extractMemberId(token); + request.setAttribute("memberId", memberId); + } catch (Exception e) { + throw new UnAuthorizedException(INVALID_ACCESS_TOKEN); + } return true; } } diff --git a/src/main/java/notai/common/exception/ErrorMessages.java b/src/main/java/notai/common/exception/ErrorMessages.java index 32b904c..ba0884a 100644 --- a/src/main/java/notai/common/exception/ErrorMessages.java +++ b/src/main/java/notai/common/exception/ErrorMessages.java @@ -9,18 +9,16 @@ public enum ErrorMessages { ANNOTATION_NOT_FOUND("주석을 찾을 수 없습니다."), // document - DOCUMENT_NOT_FOUND("자료를 찾을 수 없습니다."), + DOCUMENT_NOT_FOUND("자료를 찾을 수 없습니다."), INVALID_DOCUMENT_PAGE("존재하지 않는 페이지 입니다."), // ocr - OCR_RESULT_NOT_FOUND("OCR 데이터를 찾을 수 없습니다."), - OCR_TASK_ERROR("PDF 파일을 통해 OCR 작업을 수행하는데 실패했습니다."), + OCR_RESULT_NOT_FOUND("OCR 데이터를 찾을 수 없습니다."), OCR_TASK_ERROR("PDF 파일을 통해 OCR 작업을 수행하는데 실패했습니다."), // folder FOLDER_NOT_FOUND("폴더를 찾을 수 없습니다."), // llm task - LLM_TASK_LOG_NOT_FOUND("AI 작업 기록을 찾을 수 없습니다."), - LLM_TASK_RESULT_ERROR("AI 요약 및 문제 생성 중에 문제가 발생했습니다."), + LLM_TASK_LOG_NOT_FOUND("AI 작업 기록을 찾을 수 없습니다."), LLM_TASK_RESULT_ERROR("AI 요약 및 문제 생성 중에 문제가 발생했습니다."), // problem PROBLEM_NOT_FOUND("문제 정보를 찾을 수 없습니다."), @@ -35,24 +33,19 @@ public enum ErrorMessages { RECORDING_NOT_FOUND("녹음 파일을 찾을 수 없습니다."), // external api call - KAKAO_API_ERROR("카카오 API 호출에 예외가 발생했습니다."), - AI_SERVER_ERROR("AI 서버 API 호출에 예외가 발생했습니다."), + KAKAO_API_ERROR("카카오 API 호출에 예외가 발생했습니다."), AI_SERVER_ERROR("AI 서버 API 호출에 예외가 발생했습니다."), // auth - INVALID_ACCESS_TOKEN("유효하지 않은 토큰입니다."), - INVALID_REFRESH_TOKEN("유요하지 않은 Refresh Token입니다."), - EXPIRED_REFRESH_TOKEN("만료된 Refresh Token입니다."), - INVALID_LOGIN_TYPE("지원하지 않는 소셜 로그인 타입입니다."), + INVALID_ACCESS_TOKEN("유효하지 않은 토큰입니다."), INVALID_REFRESH_TOKEN("유요하지 않은 Refresh Token입니다."), EXPIRED_REFRESH_TOKEN( + "만료된 Refresh Token입니다."), INVALID_LOGIN_TYPE("지원하지 않는 소셜 로그인 타입입니다."), NOTFOUND_ACCESS_TOKEN( + "토큰 정보가 존재하지 않습니다."), // json conversion JSON_CONVERSION_ERROR("JSON-객체 변환 중에 오류가 발생했습니다."), // etc - INVALID_FILE_TYPE("지원하지 않는 파일 형식입니다."), - FILE_NOT_FOUND("존재하지 않는 파일입니다."), - FILE_SAVE_ERROR("파일을 저장하는 과정에서 오류가 발생했습니다."), - INVALID_AUDIO_ENCODING("오디오 파일이 잘못되었습니다.") - ; + INVALID_FILE_TYPE("지원하지 않는 파일 형식입니다."), FILE_NOT_FOUND("존재하지 않는 파일입니다."), FILE_SAVE_ERROR( + "파일을 저장하는 과정에서 오류가 발생했습니다."), INVALID_AUDIO_ENCODING("오디오 파일이 잘못되었습니다."); private final String message; diff --git a/src/main/java/notai/document/application/DocumentService.java b/src/main/java/notai/document/application/DocumentService.java index 7ebf002..d6b1f5e 100644 --- a/src/main/java/notai/document/application/DocumentService.java +++ b/src/main/java/notai/document/application/DocumentService.java @@ -15,6 +15,8 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @Service @RequiredArgsConstructor public class DocumentService { @@ -28,7 +30,7 @@ public DocumentSaveResult saveDocument( Long folderId, MultipartFile pdfFile, DocumentSaveRequest documentSaveRequest ) { PdfSaveResult pdfSaveResult = pdfService.savePdf(pdfFile); - Document document = saveAndReturnDocument(folderId, documentSaveRequest, pdfSaveResult.pdfUrl()); + Document document = saveAndReturnDocument(folderId, documentSaveRequest, pdfSaveResult); ocrService.saveOCR(document, pdfSaveResult.pdf()); return DocumentSaveResult.of(document.getId(), document.getName(), document.getUrl()); } @@ -37,7 +39,7 @@ public DocumentSaveResult saveRootDocument( MultipartFile pdfFile, DocumentSaveRequest documentSaveRequest ) { PdfSaveResult pdfSaveResult = pdfService.savePdf(pdfFile); - Document document = saveAndReturnRootDocument(documentSaveRequest, pdfSaveResult.pdfUrl()); + Document document = saveAndReturnRootDocument(documentSaveRequest, pdfSaveResult); ocrService.saveOCR(document, pdfSaveResult.pdf()); return DocumentSaveResult.of(document.getId(), document.getName(), document.getUrl()); } @@ -57,23 +59,36 @@ public void deleteDocument( ) { Document document = documentRepository.getById(documentId); document.validateDocument(folderId); + ocrService.deleteAllByDocument(document); documentRepository.delete(document); } public void deleteAllByFolder( Folder folder ) { - documentRepository.deleteAllByFolder(folder); + List documents = documentRepository.findAllByFolderId(folder.getId()); + for (Document document : documents) { + deleteDocument(folder.getId(), document.getId()); + } } - private Document saveAndReturnDocument(Long folderId, DocumentSaveRequest documentSaveRequest, String pdfUrl) { + private Document saveAndReturnDocument( + Long folderId, DocumentSaveRequest documentSaveRequest, PdfSaveResult pdfSaveResult + ) { Folder folder = folderRepository.getById(folderId); - Document document = new Document(folder, documentSaveRequest.name(), pdfUrl); + Document document = new Document(folder, + documentSaveRequest.name(), + pdfSaveResult.pdfUrl(), + pdfSaveResult.totalPages() + ); return documentRepository.save(document); } - private Document saveAndReturnRootDocument(DocumentSaveRequest documentSaveRequest, String pdfUrl) { - Document document = new Document(documentSaveRequest.name(), pdfUrl); + private Document saveAndReturnRootDocument(DocumentSaveRequest documentSaveRequest, PdfSaveResult pdfSaveResult) { + Document document = new Document(documentSaveRequest.name(), + pdfSaveResult.pdfUrl(), + pdfSaveResult.totalPages() + ); return documentRepository.save(document); } } diff --git a/src/main/java/notai/document/application/PdfService.java b/src/main/java/notai/document/application/PdfService.java deleted file mode 100644 index 74f887a..0000000 --- a/src/main/java/notai/document/application/PdfService.java +++ /dev/null @@ -1,47 +0,0 @@ -package notai.document.application; - -import lombok.RequiredArgsConstructor; -import notai.common.exception.type.FileProcessException; -import notai.common.exception.type.NotFoundException; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class PdfService { - - private static final String STORAGE_DIR = "src/main/resources/pdf/"; - - public String savePdf(MultipartFile file) { - try { - Path directoryPath = Paths.get(STORAGE_DIR); - if (!Files.exists(directoryPath)) { - Files.createDirectories(directoryPath); - } - - String fileName = UUID.randomUUID() + ".pdf"; - Path filePath = directoryPath.resolve(fileName); - file.transferTo(filePath.toFile()); - - return fileName; - } catch (IOException exception) { - throw new FileProcessException("자료를 저장하는 과정에서 에러가 발생했습니다."); - } - } - - public File getPdf(String fileName) { - Path filePath = Paths.get(STORAGE_DIR, fileName); - - if (!Files.exists(filePath)) { - throw new NotFoundException("존재하지 않는 파일입니다."); - } - return filePath.toFile(); - } -} diff --git a/src/main/java/notai/document/domain/Document.java b/src/main/java/notai/document/domain/Document.java index ff179a3..7149790 100644 --- a/src/main/java/notai/document/domain/Document.java +++ b/src/main/java/notai/document/domain/Document.java @@ -1,18 +1,18 @@ package notai.document.domain; import jakarta.persistence.*; +import static jakarta.persistence.GenerationType.IDENTITY; import jakarta.validation.constraints.NotNull; +import static lombok.AccessLevel.PROTECTED; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import notai.common.domain.RootEntity; +import static notai.common.exception.ErrorMessages.DOCUMENT_NOT_FOUND; +import static notai.common.exception.ErrorMessages.INVALID_DOCUMENT_PAGE; import notai.common.exception.type.NotFoundException; import notai.folder.domain.Folder; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; -import static notai.common.exception.ErrorMessages.DOCUMENT_NOT_FOUND; - @Slf4j @Entity @Table(name = "document") @@ -36,24 +36,35 @@ public class Document extends RootEntity { @Column(name = "url") private String url; - public Document(Folder folder, String name, String url) { + @NotNull + @Column(name = "total_pages") + private Integer totalPages; + + public Document(Folder folder, String name, String url, Integer totalPages) { this.folder = folder; this.name = name; this.url = url; + this.totalPages = totalPages; } - public Document(String name, String url) { + public Document(String name, String url, Integer totalPages) { this.name = name; this.url = url; + this.totalPages = totalPages; } public void validateDocument(Long folderId) { if (!this.folder.getId().equals(folderId)) { - log.info("요청 폴더와 실제 문서를 소유한 폴더가 다릅니다."); throw new NotFoundException(DOCUMENT_NOT_FOUND); } } + public void validatePageNumber(Integer pageNumber) { + if (totalPages < pageNumber) { + throw new NotFoundException(INVALID_DOCUMENT_PAGE); + } + } + public void updateName(String name) { this.name = name; } diff --git a/src/main/java/notai/document/domain/DocumentRepository.java b/src/main/java/notai/document/domain/DocumentRepository.java index a18272c..ddb0a36 100644 --- a/src/main/java/notai/document/domain/DocumentRepository.java +++ b/src/main/java/notai/document/domain/DocumentRepository.java @@ -1,20 +1,16 @@ package notai.document.domain; +import static notai.common.exception.ErrorMessages.DOCUMENT_NOT_FOUND; import notai.common.exception.type.NotFoundException; import notai.document.query.DocumentQueryRepository; -import notai.folder.domain.Folder; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; -import static notai.common.exception.ErrorMessages.DOCUMENT_NOT_FOUND; - public interface DocumentRepository extends JpaRepository, DocumentQueryRepository { default Document getById(Long id) { return findById(id).orElseThrow(() -> new NotFoundException(DOCUMENT_NOT_FOUND)); } List findAllByFolderId(Long folderId); - - void deleteAllByFolder(Folder folder); } diff --git a/src/main/java/notai/document/presentation/DocumentController.java b/src/main/java/notai/document/presentation/DocumentController.java index 5b5610f..c343ddd 100644 --- a/src/main/java/notai/document/presentation/DocumentController.java +++ b/src/main/java/notai/document/presentation/DocumentController.java @@ -11,16 +11,14 @@ import notai.document.presentation.response.DocumentFindResponse; import notai.document.presentation.response.DocumentSaveResponse; import notai.document.presentation.response.DocumentUpdateResponse; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.net.URI; import java.util.List; -@Controller +@RestController @RequestMapping("/api/folders/{folderId}/documents") @RequiredArgsConstructor public class DocumentController { @@ -31,7 +29,7 @@ public class DocumentController { private static final Long ROOT_FOLDER_ID = -1L; private static final String FOLDER_URL_FORMAT = "/api/folders/%s/documents/%s"; - @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + @PostMapping public ResponseEntity saveDocument( @PathVariable Long folderId, @RequestPart MultipartFile pdfFile, diff --git a/src/main/java/notai/document/presentation/PdfController.java b/src/main/java/notai/document/presentation/PdfController.java deleted file mode 100644 index 0b70edf..0000000 --- a/src/main/java/notai/document/presentation/PdfController.java +++ /dev/null @@ -1,30 +0,0 @@ -package notai.document.presentation; - -import lombok.RequiredArgsConstructor; -import notai.document.application.PdfService; -import org.springframework.core.io.FileSystemResource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; - -import java.io.File; - -@Controller -@RequestMapping("/pdf") -@RequiredArgsConstructor -public class PdfController { - - private final PdfService pdfService; - - @GetMapping("/{fileName}") - public ResponseEntity getPdf(@PathVariable String fileName) { - File pdf = pdfService.getPdf(fileName); - FileSystemResource pdfResource = new FileSystemResource(pdf); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + fileName).contentType( - MediaType.APPLICATION_PDF).body(pdfResource); - } -} diff --git a/src/main/java/notai/folder/application/FolderService.java b/src/main/java/notai/folder/application/FolderService.java index c6fcf60..ea338fa 100644 --- a/src/main/java/notai/folder/application/FolderService.java +++ b/src/main/java/notai/folder/application/FolderService.java @@ -4,10 +4,12 @@ import notai.document.application.DocumentService; import notai.folder.application.result.FolderMoveResult; import notai.folder.application.result.FolderSaveResult; +import notai.folder.application.result.FolderUpdateResult; import notai.folder.domain.Folder; import notai.folder.domain.FolderRepository; import notai.folder.presentation.request.FolderMoveRequest; import notai.folder.presentation.request.FolderSaveRequest; +import notai.folder.presentation.request.FolderUpdateRequest; import notai.member.domain.Member; import notai.member.domain.MemberRepository; import org.springframework.stereotype.Service; @@ -54,6 +56,14 @@ public FolderMoveResult moveNewParentFolder(Long memberId, Long id, FolderMoveRe return getFolderMoveResult(folder); } + public FolderUpdateResult updateFolder(Long memberId, Long id, FolderUpdateRequest folderUpdateRequest) { + Folder folder = folderRepository.getById(id); + folder.validateOwner(memberId); + folder.updateName(folderUpdateRequest.name()); + folderRepository.save(folder); + return getFolderUpdateResult(folder); + } + public void deleteFolder(Long memberId, Long id) { Folder folder = folderRepository.getById(id); folder.validateOwner(memberId); @@ -73,4 +83,9 @@ private FolderSaveResult getFolderSaveResult(Folder folder) { private FolderMoveResult getFolderMoveResult(Folder folder) { return FolderMoveResult.of(folder.getId(), folder.getName()); } + + private FolderUpdateResult getFolderUpdateResult(Folder folder) { + Long parentFolderId = folder.getParentFolder() != null ? folder.getParentFolder().getId() : null; + return FolderUpdateResult.of(folder.getId(), parentFolderId, folder.getName()); + } } diff --git a/src/main/java/notai/folder/application/result/FolderUpdateResult.java b/src/main/java/notai/folder/application/result/FolderUpdateResult.java new file mode 100644 index 0000000..3f51d92 --- /dev/null +++ b/src/main/java/notai/folder/application/result/FolderUpdateResult.java @@ -0,0 +1,11 @@ +package notai.folder.application.result; + +public record FolderUpdateResult( + Long id, + Long parentId, + String name +) { + public static FolderUpdateResult of(Long id, Long parentId, String name) { + return new FolderUpdateResult(id, parentId, name); + } +} diff --git a/src/main/java/notai/folder/domain/Folder.java b/src/main/java/notai/folder/domain/Folder.java index 005ea4b..8abfef3 100644 --- a/src/main/java/notai/folder/domain/Folder.java +++ b/src/main/java/notai/folder/domain/Folder.java @@ -1,18 +1,17 @@ package notai.folder.domain; import jakarta.persistence.*; +import static jakarta.persistence.GenerationType.IDENTITY; import jakarta.validation.constraints.NotNull; +import static lombok.AccessLevel.PROTECTED; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import notai.common.domain.RootEntity; +import static notai.common.exception.ErrorMessages.FOLDER_NOT_FOUND; import notai.common.exception.type.NotFoundException; import notai.member.domain.Member; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; -import static notai.common.exception.ErrorMessages.FOLDER_NOT_FOUND; - @Slf4j @Entity @Table(name = "folder") @@ -52,6 +51,10 @@ public void moveRootFolder() { this.parentFolder = null; } + public void updateName(String name) { + this.name = name; + } + public void moveNewParentFolder(Folder parentFolder) { this.parentFolder = parentFolder; } diff --git a/src/main/java/notai/folder/presentation/FolderController.java b/src/main/java/notai/folder/presentation/FolderController.java index 2f503dc..ab2c4af 100644 --- a/src/main/java/notai/folder/presentation/FolderController.java +++ b/src/main/java/notai/folder/presentation/FolderController.java @@ -8,19 +8,21 @@ import notai.folder.application.result.FolderFindResult; import notai.folder.application.result.FolderMoveResult; import notai.folder.application.result.FolderSaveResult; +import notai.folder.application.result.FolderUpdateResult; import notai.folder.presentation.request.FolderMoveRequest; import notai.folder.presentation.request.FolderSaveRequest; +import notai.folder.presentation.request.FolderUpdateRequest; import notai.folder.presentation.response.FolderFindResponse; import notai.folder.presentation.response.FolderMoveResponse; import notai.folder.presentation.response.FolderSaveResponse; +import notai.folder.presentation.response.FolderUpdateResponse; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.net.URI; import java.util.List; -@Controller +@RestController @RequestMapping("/api/folders") @RequiredArgsConstructor public class FolderController { @@ -46,6 +48,15 @@ public ResponseEntity moveFolder( return ResponseEntity.ok(response); } + @PutMapping("/{id}") + public ResponseEntity updateFolder( + @Auth Long memberId, @PathVariable Long id, @Valid @RequestBody FolderUpdateRequest folderUpdateRequest + ) { + FolderUpdateResult folderResult = folderService.updateFolder(memberId, id, folderUpdateRequest); + FolderUpdateResponse response = FolderUpdateResponse.from(folderResult); + return ResponseEntity.ok(response); + } + @GetMapping public ResponseEntity> getFolders( @Auth Long memberId, @RequestParam(required = false) Long parentFolderId diff --git a/src/main/java/notai/folder/presentation/request/FolderUpdateRequest.java b/src/main/java/notai/folder/presentation/request/FolderUpdateRequest.java new file mode 100644 index 0000000..5a753cc --- /dev/null +++ b/src/main/java/notai/folder/presentation/request/FolderUpdateRequest.java @@ -0,0 +1,6 @@ +package notai.folder.presentation.request; + +public record FolderUpdateRequest( + String name +) { +} diff --git a/src/main/java/notai/folder/presentation/response/FolderUpdateResponse.java b/src/main/java/notai/folder/presentation/response/FolderUpdateResponse.java new file mode 100644 index 0000000..dae6065 --- /dev/null +++ b/src/main/java/notai/folder/presentation/response/FolderUpdateResponse.java @@ -0,0 +1,17 @@ +package notai.folder.presentation.response; + +import notai.folder.application.result.FolderUpdateResult; + +public record FolderUpdateResponse( + Long id, + Long parentId, + String name +) { + public static FolderUpdateResponse from(FolderUpdateResult folderUpdateResult) { + return new FolderUpdateResponse( + folderUpdateResult.id(), + folderUpdateResult.parentId(), + folderUpdateResult.name() + ); + } +} diff --git a/src/main/java/notai/llm/application/LLMQueryService.java b/src/main/java/notai/llm/application/LLMQueryService.java deleted file mode 100644 index 0a9b6cd..0000000 --- a/src/main/java/notai/llm/application/LLMQueryService.java +++ /dev/null @@ -1,118 +0,0 @@ -package notai.llm.application; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import notai.common.exception.type.InternalServerErrorException; -import notai.common.exception.type.NotFoundException; -import notai.document.domain.DocumentRepository; -import notai.llm.application.result.LLMResultsResult; -import notai.llm.application.result.LLMResultsResult.LLMContent; -import notai.llm.application.result.LLMResultsResult.LLMResult; -import notai.llm.application.result.LLMStatusResult; -import notai.llm.domain.TaskStatus; -import notai.llm.query.LLMQueryRepository; -import notai.problem.query.ProblemQueryRepository; -import notai.problem.query.result.ProblemPageContentResult; -import notai.summary.query.SummaryQueryRepository; -import notai.summary.query.result.SummaryPageContentResult; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.List; - -import static notai.common.exception.ErrorMessages.*; -import static notai.llm.domain.TaskStatus.COMPLETED; -import static notai.llm.domain.TaskStatus.IN_PROGRESS; - -@Slf4j -@Service -@RequiredArgsConstructor -public class LLMQueryService { - - private final LLMQueryRepository llmQueryRepository; - private final DocumentRepository documentRepository; - private final SummaryQueryRepository summaryQueryRepository; - private final ProblemQueryRepository problemQueryRepository; - - public LLMStatusResult fetchTaskStatus(Long documentId) { - checkDocumentExists(documentId); - List summaryIds = getSummaryIds(documentId); - List taskStatuses = getTaskStatuses(summaryIds); - - int totalPages = summaryIds.size(); - int completedPages = Collections.frequency(taskStatuses, COMPLETED); - - if (totalPages == completedPages) { - return LLMStatusResult.of(documentId, COMPLETED, totalPages, completedPages); - } - return LLMStatusResult.of(documentId, IN_PROGRESS, totalPages, completedPages); - } - - public LLMResultsResult findTaskResult(Long documentId) { - checkDocumentExists(documentId); - List summaryResults = getSummaryPageContentResults(documentId); - List problemResults = getProblemPageContentResults(documentId); - checkSummaryAndProblemCountsEqual(summaryResults, problemResults); - - List results = summaryResults.stream().map(summaryResult -> { - LLMContent content = LLMContent.of( - summaryResult.content(), - findProblemContentByPageNumber(problemResults, summaryResult.pageNumber()) - ); - return LLMResult.of(summaryResult.pageNumber(), content); - }).toList(); - - return LLMResultsResult.of(documentId, results); - } - - private void checkDocumentExists(Long documentId) { - if (!documentRepository.existsById(documentId)) { - throw new NotFoundException(DOCUMENT_NOT_FOUND); - } - } - - private static void checkSummaryAndProblemCountsEqual( - List summaryResults, List problemResults - ) { - if (summaryResults.size() != problemResults.size()) { - log.error("요약 개수와 문제 개수가 일치하지 않습니다. 요약: {} 개, 문제: {} 개", summaryResults.size(), problemResults.size()); - throw new InternalServerErrorException(LLM_TASK_RESULT_ERROR); - } - } - - private List getSummaryIds(Long documentId) { - List summaryIds = summaryQueryRepository.getSummaryIdsByDocumentId(documentId); - if (summaryIds.isEmpty()) { - throw new NotFoundException(LLM_TASK_LOG_NOT_FOUND); - } - return summaryIds; - } - - private List getTaskStatuses(List summaryIds) { - return summaryIds.stream().map(llmQueryRepository::getTaskStatusBySummaryId).toList(); - } - - private List getSummaryPageContentResults(Long documentId) { - List summaryResults = summaryQueryRepository.getPageNumbersAndContentByDocumentId( - documentId); - if (summaryResults.isEmpty()) { - throw new NotFoundException(LLM_TASK_LOG_NOT_FOUND); - } - return summaryResults; - } - - private List getProblemPageContentResults(Long documentId) { - return problemQueryRepository.getPageNumbersAndContentByDocumentId(documentId); - } - - private String findProblemContentByPageNumber(List results, int pageNumber) { - return results.stream() - .filter(result -> result.pageNumber() == pageNumber) - .findFirst() - .map(ProblemPageContentResult::content) - .orElseThrow(() -> { - log.error("{} 페이지에 대한 문제 생성 결과가 없습니다.", pageNumber); - return new InternalServerErrorException(LLM_TASK_RESULT_ERROR); - }); - } -} diff --git a/src/main/java/notai/llm/application/LlmTaskQueryService.java b/src/main/java/notai/llm/application/LlmTaskQueryService.java new file mode 100644 index 0000000..39c642a --- /dev/null +++ b/src/main/java/notai/llm/application/LlmTaskQueryService.java @@ -0,0 +1,151 @@ +package notai.llm.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import static notai.common.exception.ErrorMessages.DOCUMENT_NOT_FOUND; +import static notai.common.exception.ErrorMessages.LLM_TASK_RESULT_ERROR; +import notai.common.exception.type.InternalServerErrorException; +import notai.common.exception.type.NotFoundException; +import notai.document.domain.DocumentRepository; +import notai.llm.application.command.LlmTaskPageResultCommand; +import notai.llm.application.command.LlmTaskPageStatusCommand; +import notai.llm.application.result.LlmTaskAllPagesResult; +import notai.llm.application.result.LlmTaskAllPagesResult.LlmContent; +import notai.llm.application.result.LlmTaskAllPagesResult.LlmResult; +import notai.llm.application.result.LlmTaskOverallStatusResult; +import notai.llm.application.result.LlmTaskPageResult; +import notai.llm.application.result.LlmTaskPageStatusResult; +import notai.llm.domain.TaskStatus; +import static notai.llm.domain.TaskStatus.*; +import notai.llm.query.LlmTaskQueryRepository; +import notai.problem.domain.ProblemRepository; +import notai.problem.query.result.ProblemPageContentResult; +import notai.summary.domain.SummaryRepository; +import notai.summary.query.result.SummaryPageContentResult; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LlmTaskQueryService { + + private final LlmTaskQueryRepository llmTaskQueryRepository; + private final DocumentRepository documentRepository; + private final SummaryRepository summaryRepository; + private final ProblemRepository problemRepository; + + public LlmTaskOverallStatusResult fetchOverallStatus(Long documentId) { + checkDocumentExists(documentId); + List summaryIds = summaryRepository.getSummaryIdsByDocumentId(documentId); + + if (summaryIds.isEmpty()) { + return LlmTaskOverallStatusResult.of(documentId, NOT_REQUESTED, 0, 0); + } + + List taskStatuses = getTaskStatuses(summaryIds); + + int totalPages = summaryIds.size(); + int completedPages = Collections.frequency(taskStatuses, COMPLETED); + + if (totalPages == completedPages) { + return LlmTaskOverallStatusResult.of(documentId, COMPLETED, totalPages, completedPages); + } + return LlmTaskOverallStatusResult.of(documentId, IN_PROGRESS, totalPages, completedPages); + } + + public LlmTaskPageStatusResult fetchPageStatus(LlmTaskPageStatusCommand command) { // TODO: 페이지 번호 검증 추가 + checkDocumentExists(command.documentId()); + Long summaryId = + summaryRepository.getSummaryIdByDocumentIdAndPageNumber(command.documentId(), command.pageNumber()); + + if (summaryId == null) { + return LlmTaskPageStatusResult.from(NOT_REQUESTED); + } + return LlmTaskPageStatusResult.from(llmTaskQueryRepository.getTaskStatusBySummaryId(summaryId)); + } + + public LlmTaskAllPagesResult findAllPagesResult(Long documentId) { + checkDocumentExists(documentId); + + List summaryResults = + summaryRepository.getPageNumbersAndContentByDocumentId(documentId); + List problemResults = + problemRepository.getPageNumbersAndContentByDocumentId(documentId); + + checkSummaryAndProblemCountsEqual(summaryResults, problemResults); + + if (summaryResults.isEmpty()) { + return LlmTaskAllPagesResult.of(documentId, Collections.emptyList()); + } + + List results = summaryResults.stream().map(summaryResult -> { + LlmContent content = LlmContent.of( + summaryResult.content(), + findProblemContentByPageNumber(problemResults, summaryResult.pageNumber()) + ); + return LlmResult.of(summaryResult.pageNumber(), content); + }).toList(); + + return LlmTaskAllPagesResult.of(documentId, results); + } + + public LlmTaskPageResult findPageResult(LlmTaskPageResultCommand command) { // TODO: 페이지 번호 검증 추가 + checkDocumentExists(command.documentId()); + + String summaryResult = summaryRepository.getSummaryContentByDocumentIdAndPageNumber( + command.documentId(), command.pageNumber()); + String problemResult = problemRepository.getProblemContentByDocumentIdAndPageNumber( + command.documentId(), command.pageNumber()); + + checkSummaryAndProblemConsistency(command, summaryResult, problemResult); + + return LlmTaskPageResult.of(summaryResult, problemResult); + } + + private static void checkSummaryAndProblemConsistency( + LlmTaskPageResultCommand command, String summaryResult, + String problemResult) { + if (summaryResult == null && problemResult != null) { + log.error("요약과 문제 생성 결과가 매칭되지 않습니다. {} 페이지에 대한 요약 결과가 없습니다.", command.pageNumber()); + throw new InternalServerErrorException(LLM_TASK_RESULT_ERROR); + } + + if (problemResult == null && summaryResult != null) { + log.error("요약과 문제 생성 결과가 매칭되지 않습니다. {} 페이지에 대한 문제 생성 결과가 없습니다.", command.pageNumber()); + throw new InternalServerErrorException(LLM_TASK_RESULT_ERROR); + } + } + + private void checkDocumentExists(Long documentId) { + if (!documentRepository.existsById(documentId)) { + throw new NotFoundException(DOCUMENT_NOT_FOUND); + } + } + + private static void checkSummaryAndProblemCountsEqual( + List summaryResults, List problemResults + ) { + if (summaryResults.size() != problemResults.size()) { + log.error("요약 개수와 문제 개수가 일치하지 않습니다. 요약: {} 개, 문제: {} 개", summaryResults.size(), problemResults.size()); + throw new InternalServerErrorException(LLM_TASK_RESULT_ERROR); + } + } + + private List getTaskStatuses(List summaryIds) { + return summaryIds.stream().map(llmTaskQueryRepository::getTaskStatusBySummaryId).toList(); + } + + private String findProblemContentByPageNumber(List results, int pageNumber) { + return results.stream() + .filter(result -> result.pageNumber() == pageNumber) + .findFirst() + .map(ProblemPageContentResult::content) + .orElseThrow(() -> { + log.error("요약과 문제 생성 결과가 매칭되지 않습니다. {} 페이지에 대한 문제 생성 결과가 없습니다.", pageNumber); + return new InternalServerErrorException(LLM_TASK_RESULT_ERROR); + }); + } +} diff --git a/src/main/java/notai/llm/application/LLMService.java b/src/main/java/notai/llm/application/LlmTaskService.java similarity index 57% rename from src/main/java/notai/llm/application/LLMService.java rename to src/main/java/notai/llm/application/LlmTaskService.java index 6986a3b..b6f41f3 100644 --- a/src/main/java/notai/llm/application/LLMService.java +++ b/src/main/java/notai/llm/application/LlmTaskService.java @@ -8,11 +8,11 @@ import notai.client.ai.request.LlmTaskRequest; import notai.document.domain.Document; import notai.document.domain.DocumentRepository; -import notai.llm.application.command.LLMSubmitCommand; +import notai.llm.application.command.LlmTaskSubmitCommand; import notai.llm.application.command.SummaryAndProblemUpdateCommand; -import notai.llm.application.result.LLMSubmitResult; -import notai.llm.domain.LLM; -import notai.llm.domain.LLMRepository; +import notai.llm.application.result.LlmTaskSubmitResult; +import notai.llm.domain.LlmTask; +import notai.llm.domain.LlmTaskRepository; import notai.problem.domain.Problem; import notai.problem.domain.ProblemRepository; import notai.summary.domain.Summary; @@ -23,6 +23,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -34,16 +35,16 @@ @Service @Transactional @RequiredArgsConstructor -public class LLMService { +public class LlmTaskService { - private final LLMRepository llmRepository; + private final LlmTaskRepository llmTaskRepository; private final DocumentRepository documentRepository; private final SummaryRepository summaryRepository; private final ProblemRepository problemRepository; private final AnnotationRepository annotationRepository; private final AiClient aiClient; - public LLMSubmitResult submitTask(LLMSubmitCommand command) { + public LlmTaskSubmitResult submitTasks(LlmTaskSubmitCommand command) { // TODO: 페이지 번호 검증 추가 Document foundDocument = documentRepository.getById(command.documentId()); List annotations = annotationRepository.findByDocumentId(command.documentId()); @@ -51,25 +52,42 @@ public LLMSubmitResult submitTask(LLMSubmitCommand command) { annotations.stream().collect(groupingBy(Annotation::getPageNumber)); command.pages().forEach(pageNumber -> { - String annotationContents = annotationsByPage.getOrDefault( - pageNumber, - List.of() - ).stream().map(Annotation::getContent).collect(Collectors.joining(", ")); + submitPageTask(pageNumber, annotationsByPage, foundDocument); + }); + + return LlmTaskSubmitResult.of(command.documentId(), LocalDateTime.now()); + } + + private void submitPageTask(Integer pageNumber, Map> annotationsByPage, Document foundDocument) { + String annotationContents = annotationsByPage.getOrDefault( + pageNumber, + List.of() + ).stream().map(Annotation::getContent).collect(Collectors.joining(", ")); - // Todo OCR, STT 결과 전달 - UUID taskId = sendRequestToAIServer("ocrText", "stt", annotationContents); + // Todo OCR, STT 결과 전달 + UUID taskId = sendRequestToAIServer("ocrText", "stt", annotationContents); + + Optional foundSummary = summaryRepository.findByDocumentAndPageNumber(foundDocument, pageNumber); + Optional foundProblem = problemRepository.findByDocumentAndPageNumber(foundDocument, pageNumber); + + if (foundSummary.isEmpty() && foundProblem.isEmpty()) { Summary summary = new Summary(foundDocument, pageNumber); Problem problem = new Problem(foundDocument, pageNumber); - LLM taskRecord = new LLM(taskId, summary, problem); - llmRepository.save(taskRecord); - }); + LlmTask taskRecord = new LlmTask(taskId, summary, problem); + llmTaskRepository.save(taskRecord); + } + if (foundSummary.isPresent() && foundProblem.isPresent()) { + LlmTask foundTaskRecord = llmTaskRepository.getBySummaryAndProblem(foundSummary.get(), foundProblem.get()); + llmTaskRepository.delete(foundTaskRecord); - return LLMSubmitResult.of(command.documentId(), LocalDateTime.now()); + LlmTask taskRecord = new LlmTask(taskId, foundSummary.get(), foundProblem.get()); + llmTaskRepository.save(taskRecord); + } } public Integer updateSummaryAndProblem(SummaryAndProblemUpdateCommand command) { - LLM taskRecord = llmRepository.getById(command.taskId()); + LlmTask taskRecord = llmTaskRepository.getById(command.taskId()); Summary foundSummary = summaryRepository.getById(taskRecord.getSummary().getId()); Problem foundProblem = problemRepository.getById(taskRecord.getProblem().getId()); @@ -77,11 +95,11 @@ public Integer updateSummaryAndProblem(SummaryAndProblemUpdateCommand command) { foundSummary.updateContent(command.summary()); foundProblem.updateContent(command.problem()); - llmRepository.save(taskRecord); + llmTaskRepository.save(taskRecord); summaryRepository.save(foundSummary); problemRepository.save(foundProblem); - return command.pageNumber(); + return foundSummary.getPageNumber(); } private UUID sendRequestToAIServer(String ocrText, String stt, String keyboardNote) { diff --git a/src/main/java/notai/llm/application/command/LlmTaskPageResultCommand.java b/src/main/java/notai/llm/application/command/LlmTaskPageResultCommand.java new file mode 100644 index 0000000..7f7ceaf --- /dev/null +++ b/src/main/java/notai/llm/application/command/LlmTaskPageResultCommand.java @@ -0,0 +1,10 @@ +package notai.llm.application.command; + +public record LlmTaskPageResultCommand( + Long documentId, + Integer pageNumber +) { + public static LlmTaskPageResultCommand of(Long documentId, Integer pageNumber) { + return new LlmTaskPageResultCommand(documentId, pageNumber); + } +} diff --git a/src/main/java/notai/llm/application/command/LlmTaskPageStatusCommand.java b/src/main/java/notai/llm/application/command/LlmTaskPageStatusCommand.java new file mode 100644 index 0000000..ca8cf79 --- /dev/null +++ b/src/main/java/notai/llm/application/command/LlmTaskPageStatusCommand.java @@ -0,0 +1,10 @@ +package notai.llm.application.command; + +public record LlmTaskPageStatusCommand( + Long documentId, + Integer pageNumber +) { + public static LlmTaskPageStatusCommand of(Long documentId, Integer pageNumber) { + return new LlmTaskPageStatusCommand(documentId, pageNumber); + } +} diff --git a/src/main/java/notai/llm/application/command/LLMSubmitCommand.java b/src/main/java/notai/llm/application/command/LlmTaskSubmitCommand.java similarity index 77% rename from src/main/java/notai/llm/application/command/LLMSubmitCommand.java rename to src/main/java/notai/llm/application/command/LlmTaskSubmitCommand.java index 3e7b329..49d83b3 100644 --- a/src/main/java/notai/llm/application/command/LLMSubmitCommand.java +++ b/src/main/java/notai/llm/application/command/LlmTaskSubmitCommand.java @@ -2,7 +2,7 @@ import java.util.List; -public record LLMSubmitCommand( +public record LlmTaskSubmitCommand( Long documentId, List pages ) { diff --git a/src/main/java/notai/llm/application/command/SummaryAndProblemUpdateCommand.java b/src/main/java/notai/llm/application/command/SummaryAndProblemUpdateCommand.java index 420e5d4..d292b14 100644 --- a/src/main/java/notai/llm/application/command/SummaryAndProblemUpdateCommand.java +++ b/src/main/java/notai/llm/application/command/SummaryAndProblemUpdateCommand.java @@ -4,7 +4,6 @@ public record SummaryAndProblemUpdateCommand( UUID taskId, - Integer pageNumber, String summary, String problem ) { diff --git a/src/main/java/notai/llm/application/result/LLMResultsResult.java b/src/main/java/notai/llm/application/result/LLMResultsResult.java deleted file mode 100644 index 63dfcaa..0000000 --- a/src/main/java/notai/llm/application/result/LLMResultsResult.java +++ /dev/null @@ -1,31 +0,0 @@ -package notai.llm.application.result; - -import java.util.List; - -public record LLMResultsResult( - Long documentId, - Integer totalPages, - List results -) { - public static LLMResultsResult of(Long documentId, List results) { - return new LLMResultsResult(documentId, results.size(), results); - } - - public record LLMResult( - Integer pageNumber, - LLMContent content - ) { - public static LLMResult of(Integer pageNumber, LLMContent content) { - return new LLMResult(pageNumber, content); - } - } - - public record LLMContent( - String summary, - String problem - ) { - public static LLMContent of(String summary, String problem) { - return new LLMContent(summary, problem); - } - } -} diff --git a/src/main/java/notai/llm/application/result/LLMSubmitResult.java b/src/main/java/notai/llm/application/result/LLMSubmitResult.java deleted file mode 100644 index ab0c2ac..0000000 --- a/src/main/java/notai/llm/application/result/LLMSubmitResult.java +++ /dev/null @@ -1,12 +0,0 @@ -package notai.llm.application.result; - -import java.time.LocalDateTime; - -public record LLMSubmitResult( - Long documentId, - LocalDateTime createdAt -) { - public static LLMSubmitResult of(Long documentId, LocalDateTime createdAt) { - return new LLMSubmitResult(documentId, createdAt); - } -} diff --git a/src/main/java/notai/llm/application/result/LlmTaskAllPagesResult.java b/src/main/java/notai/llm/application/result/LlmTaskAllPagesResult.java new file mode 100644 index 0000000..576a041 --- /dev/null +++ b/src/main/java/notai/llm/application/result/LlmTaskAllPagesResult.java @@ -0,0 +1,31 @@ +package notai.llm.application.result; + +import java.util.List; + +public record LlmTaskAllPagesResult( + Long documentId, + Integer totalPages, + List results +) { + public static LlmTaskAllPagesResult of(Long documentId, List results) { + return new LlmTaskAllPagesResult(documentId, results.size(), results); + } + + public record LlmResult( + Integer pageNumber, + LlmContent content + ) { + public static LlmResult of(Integer pageNumber, LlmContent content) { + return new LlmResult(pageNumber, content); + } + } + + public record LlmContent( + String summary, + String problem + ) { + public static LlmContent of(String summary, String problem) { + return new LlmContent(summary, problem); + } + } +} diff --git a/src/main/java/notai/llm/application/result/LLMStatusResult.java b/src/main/java/notai/llm/application/result/LlmTaskOverallStatusResult.java similarity index 61% rename from src/main/java/notai/llm/application/result/LLMStatusResult.java rename to src/main/java/notai/llm/application/result/LlmTaskOverallStatusResult.java index 158e099..c6e8b03 100644 --- a/src/main/java/notai/llm/application/result/LLMStatusResult.java +++ b/src/main/java/notai/llm/application/result/LlmTaskOverallStatusResult.java @@ -2,15 +2,15 @@ import notai.llm.domain.TaskStatus; -public record LLMStatusResult( +public record LlmTaskOverallStatusResult( Long documentId, TaskStatus overallStatus, Integer totalPages, Integer completedPages ) { - public static LLMStatusResult of( + public static LlmTaskOverallStatusResult of( Long documentId, TaskStatus overallStatus, Integer totalPages, Integer completedPages ) { - return new LLMStatusResult(documentId, overallStatus, totalPages, completedPages); + return new LlmTaskOverallStatusResult(documentId, overallStatus, totalPages, completedPages); } } diff --git a/src/main/java/notai/llm/application/result/LlmTaskPageResult.java b/src/main/java/notai/llm/application/result/LlmTaskPageResult.java new file mode 100644 index 0000000..7b8f4ee --- /dev/null +++ b/src/main/java/notai/llm/application/result/LlmTaskPageResult.java @@ -0,0 +1,10 @@ +package notai.llm.application.result; + +public record LlmTaskPageResult( + String summary, + String problem +) { + public static LlmTaskPageResult of(String summary, String problem) { + return new LlmTaskPageResult(summary, problem); + } +} diff --git a/src/main/java/notai/llm/application/result/LlmTaskPageStatusResult.java b/src/main/java/notai/llm/application/result/LlmTaskPageStatusResult.java new file mode 100644 index 0000000..81f4381 --- /dev/null +++ b/src/main/java/notai/llm/application/result/LlmTaskPageStatusResult.java @@ -0,0 +1,11 @@ +package notai.llm.application.result; + +import notai.llm.domain.TaskStatus; + +public record LlmTaskPageStatusResult( + TaskStatus status +) { + public static LlmTaskPageStatusResult from(TaskStatus status) { + return new LlmTaskPageStatusResult(status); + } +} diff --git a/src/main/java/notai/llm/application/result/LlmTaskSubmitResult.java b/src/main/java/notai/llm/application/result/LlmTaskSubmitResult.java new file mode 100644 index 0000000..cdfdfc8 --- /dev/null +++ b/src/main/java/notai/llm/application/result/LlmTaskSubmitResult.java @@ -0,0 +1,12 @@ +package notai.llm.application.result; + +import java.time.LocalDateTime; + +public record LlmTaskSubmitResult( + Long documentId, + LocalDateTime createdAt +) { + public static LlmTaskSubmitResult of(Long documentId, LocalDateTime createdAt) { + return new LlmTaskSubmitResult(documentId, createdAt); + } +} diff --git a/src/main/java/notai/llm/domain/LLMRepository.java b/src/main/java/notai/llm/domain/LLMRepository.java deleted file mode 100644 index 9f3cde1..0000000 --- a/src/main/java/notai/llm/domain/LLMRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package notai.llm.domain; - -import notai.common.exception.type.NotFoundException; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.UUID; - -import static notai.common.exception.ErrorMessages.LLM_TASK_LOG_NOT_FOUND; - -public interface LLMRepository extends JpaRepository { - default LLM getById(UUID id) { - return findById(id).orElseThrow(() -> new NotFoundException(LLM_TASK_LOG_NOT_FOUND)); - } -} diff --git a/src/main/java/notai/llm/domain/LLM.java b/src/main/java/notai/llm/domain/LlmTask.java similarity index 70% rename from src/main/java/notai/llm/domain/LLM.java rename to src/main/java/notai/llm/domain/LlmTask.java index f93c53f..24233d8 100644 --- a/src/main/java/notai/llm/domain/LLM.java +++ b/src/main/java/notai/llm/domain/LlmTask.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; +import static lombok.AccessLevel.PROTECTED; import lombok.Getter; import lombok.NoArgsConstructor; import notai.common.domain.RootEntity; @@ -10,28 +11,26 @@ import java.util.UUID; -import static lombok.AccessLevel.PROTECTED; - /** - * 요약과 문제 생성을 하는 LLM 모델의 작업 기록을 저장하는 테이블입니다. + * 요약과 문제 생성을 하는 LlmTask 모델의 작업 기록을 저장하는 테이블입니다. */ @Getter @NoArgsConstructor(access = PROTECTED) @Entity -@Table(name = "llm") -public class LLM extends RootEntity { +@Table(name = "llm_task") +public class LlmTask extends RootEntity { @Id private UUID id; @NotNull - @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) @JoinColumn(name = "summary_id") private Summary summary; @NotNull - @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) @JoinColumn(name = "problem_id") private Problem problem; @@ -40,7 +39,7 @@ public class LLM extends RootEntity { @Column(length = 20) private TaskStatus status; - public LLM(UUID id, Summary summary, Problem problem) { + public LlmTask(UUID id, Summary summary, Problem problem) { this.id = id; this.summary = summary; this.problem = problem; diff --git a/src/main/java/notai/llm/domain/LlmTaskRepository.java b/src/main/java/notai/llm/domain/LlmTaskRepository.java new file mode 100644 index 0000000..5440986 --- /dev/null +++ b/src/main/java/notai/llm/domain/LlmTaskRepository.java @@ -0,0 +1,23 @@ +package notai.llm.domain; + +import static notai.common.exception.ErrorMessages.LLM_TASK_LOG_NOT_FOUND; +import notai.common.exception.type.NotFoundException; +import notai.problem.domain.Problem; +import notai.summary.domain.Summary; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface LlmTaskRepository extends JpaRepository { + default LlmTask getById(UUID id) { + return findById(id).orElseThrow(() -> new NotFoundException(LLM_TASK_LOG_NOT_FOUND)); + } + + Optional findBySummaryAndProblem(Summary summary, Problem problem); + + default LlmTask getBySummaryAndProblem(Summary summary, Problem problem) { + return findBySummaryAndProblem(summary, problem) + .orElseThrow(() -> new NotFoundException(LLM_TASK_LOG_NOT_FOUND)); + } +} diff --git a/src/main/java/notai/llm/domain/TaskStatus.java b/src/main/java/notai/llm/domain/TaskStatus.java index aa0b6dd..5c79ec6 100644 --- a/src/main/java/notai/llm/domain/TaskStatus.java +++ b/src/main/java/notai/llm/domain/TaskStatus.java @@ -1,5 +1,5 @@ package notai.llm.domain; public enum TaskStatus { - PENDING, IN_PROGRESS, COMPLETED + PENDING, IN_PROGRESS, COMPLETED, NOT_REQUESTED } diff --git a/src/main/java/notai/llm/presentation/LLMController.java b/src/main/java/notai/llm/presentation/LLMController.java deleted file mode 100644 index 5d8a6e9..0000000 --- a/src/main/java/notai/llm/presentation/LLMController.java +++ /dev/null @@ -1,56 +0,0 @@ -package notai.llm.presentation; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import notai.llm.application.LLMQueryService; -import notai.llm.application.LLMService; -import notai.llm.application.command.LLMSubmitCommand; -import notai.llm.application.command.SummaryAndProblemUpdateCommand; -import notai.llm.application.result.LLMResultsResult; -import notai.llm.application.result.LLMStatusResult; -import notai.llm.application.result.LLMSubmitResult; -import notai.llm.presentation.request.LLMSubmitRequest; -import notai.llm.presentation.request.SummaryAndProblemUpdateRequest; -import notai.llm.presentation.response.LLMResultsResponse; -import notai.llm.presentation.response.LLMStatusResponse; -import notai.llm.presentation.response.LLMSubmitResponse; -import notai.llm.presentation.response.SummaryAndProblemUpdateResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/ai/llm") -@RequiredArgsConstructor -public class LLMController { - - private final LLMService llmService; - private final LLMQueryService llmQueryService; - - @PostMapping - public ResponseEntity submitTask(@RequestBody @Valid LLMSubmitRequest request) { - LLMSubmitCommand command = request.toCommand(); - LLMSubmitResult result = llmService.submitTask(command); - return ResponseEntity.accepted().body(LLMSubmitResponse.from(result)); - } - - @GetMapping("/status/{documentId}") - public ResponseEntity fetchTaskStatus(@PathVariable("documentId") Long documentId) { - LLMStatusResult result = llmQueryService.fetchTaskStatus(documentId); - return ResponseEntity.ok(LLMStatusResponse.from(result)); - } - - @GetMapping("/results/{documentId}") - public ResponseEntity findTaskResult(@PathVariable("documentId") Long documentId) { - LLMResultsResult result = llmQueryService.findTaskResult(documentId); - return ResponseEntity.ok(LLMResultsResponse.from(result)); - } - - @PostMapping("/callback") - public ResponseEntity handleTaskCallback( - @RequestBody @Valid SummaryAndProblemUpdateRequest request - ) { - SummaryAndProblemUpdateCommand command = request.toCommand(); - Integer receivedPage = llmService.updateSummaryAndProblem(command); - return ResponseEntity.ok(SummaryAndProblemUpdateResponse.from(receivedPage)); - } -} diff --git a/src/main/java/notai/llm/presentation/LlmTaskController.java b/src/main/java/notai/llm/presentation/LlmTaskController.java new file mode 100644 index 0000000..352698b --- /dev/null +++ b/src/main/java/notai/llm/presentation/LlmTaskController.java @@ -0,0 +1,70 @@ +package notai.llm.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import notai.llm.application.LlmTaskQueryService; +import notai.llm.application.LlmTaskService; +import notai.llm.application.command.LlmTaskPageResultCommand; +import notai.llm.application.command.LlmTaskPageStatusCommand; +import notai.llm.application.command.LlmTaskSubmitCommand; +import notai.llm.application.command.SummaryAndProblemUpdateCommand; +import notai.llm.application.result.*; +import notai.llm.presentation.request.LlmTaskSubmitRequest; +import notai.llm.presentation.request.SummaryAndProblemUpdateRequest; +import notai.llm.presentation.response.*; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/ai/llm") +@RequiredArgsConstructor +public class LlmTaskController { + + private final LlmTaskService llmTaskService; + private final LlmTaskQueryService llmTaskQueryService; + + @PostMapping + public ResponseEntity submitTask(@RequestBody @Valid LlmTaskSubmitRequest request) { + LlmTaskSubmitCommand command = request.toCommand(); + LlmTaskSubmitResult result = llmTaskService.submitTasks(command); + return ResponseEntity.accepted().body(LlmTaskSubmitResponse.from(result)); + } + + @GetMapping("/status/{documentId}") + public ResponseEntity fetchOverallStatus(@PathVariable("documentId") Long documentId) { + LlmTaskOverallStatusResult result = llmTaskQueryService.fetchOverallStatus(documentId); + return ResponseEntity.ok(LlmTaskOverallStatusResponse.from(result)); + } + + @GetMapping("/status/{documentId}/{pageNumber}") + public ResponseEntity fetchPageStatus( + @PathVariable("documentId") Long documentId, @PathVariable("pageNumber") Integer pageNumber + ) { + LlmTaskPageStatusCommand command = LlmTaskPageStatusCommand.of(documentId, pageNumber); + LlmTaskPageStatusResult result = llmTaskQueryService.fetchPageStatus(command); + return ResponseEntity.ok(LlmTaskPageStatusResponse.from(result)); + } + + @GetMapping("/results/{documentId}") + public ResponseEntity findAllPagesResult(@PathVariable("documentId") Long documentId) { + LlmTaskAllPagesResult result = llmTaskQueryService.findAllPagesResult(documentId); + return ResponseEntity.ok(LlmTaskAllPagesResultResponse.from(result)); + } + + @GetMapping("/results/{documentId}/{pageNumber}") + public ResponseEntity findPageResult( + @PathVariable("documentId") Long documentId, @PathVariable("pageNumber") Integer pageNumber) { + LlmTaskPageResultCommand command = LlmTaskPageResultCommand.of(documentId, pageNumber); + LlmTaskPageResult result = llmTaskQueryService.findPageResult(command); + return ResponseEntity.ok(LlmTaskPageResultResponse.from(result)); + } + + @PostMapping("/callback") + public ResponseEntity handleTaskCallback( + @RequestBody @Valid SummaryAndProblemUpdateRequest request + ) { + SummaryAndProblemUpdateCommand command = request.toCommand(); + Integer receivedPage = llmTaskService.updateSummaryAndProblem(command); + return ResponseEntity.ok(SummaryAndProblemUpdateResponse.from(receivedPage)); + } +} diff --git a/src/main/java/notai/llm/presentation/request/LLMSubmitRequest.java b/src/main/java/notai/llm/presentation/request/LlmTaskSubmitRequest.java similarity index 64% rename from src/main/java/notai/llm/presentation/request/LLMSubmitRequest.java rename to src/main/java/notai/llm/presentation/request/LlmTaskSubmitRequest.java index 1226bb5..7c03443 100644 --- a/src/main/java/notai/llm/presentation/request/LLMSubmitRequest.java +++ b/src/main/java/notai/llm/presentation/request/LlmTaskSubmitRequest.java @@ -2,17 +2,17 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; -import notai.llm.application.command.LLMSubmitCommand; +import notai.llm.application.command.LlmTaskSubmitCommand; import java.util.List; -public record LLMSubmitRequest( +public record LlmTaskSubmitRequest( @NotNull(message = "문서 ID는 필수 입력 값입니다.") Long documentId, List<@Positive(message = "페이지 번호는 양수여야 합니다.") Integer> pages ) { - public LLMSubmitCommand toCommand() { - return new LLMSubmitCommand(documentId, pages); + public LlmTaskSubmitCommand toCommand() { + return new LlmTaskSubmitCommand(documentId, pages); } } diff --git a/src/main/java/notai/llm/presentation/request/SummaryAndProblemUpdateRequest.java b/src/main/java/notai/llm/presentation/request/SummaryAndProblemUpdateRequest.java index 1eba697..c2c6bd0 100644 --- a/src/main/java/notai/llm/presentation/request/SummaryAndProblemUpdateRequest.java +++ b/src/main/java/notai/llm/presentation/request/SummaryAndProblemUpdateRequest.java @@ -2,25 +2,18 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; import notai.llm.application.command.SummaryAndProblemUpdateCommand; import java.util.UUID; public record SummaryAndProblemUpdateRequest( - UUID taskId, - - @NotNull Long documentId, - - Integer totalPages, - - @NotNull @Positive Integer pageNumber, + @NotNull UUID taskId, @NotBlank String summary, @NotBlank String problem ) { public SummaryAndProblemUpdateCommand toCommand() { - return new SummaryAndProblemUpdateCommand(taskId, pageNumber, summary, problem); + return new SummaryAndProblemUpdateCommand(taskId, summary, problem); } } diff --git a/src/main/java/notai/llm/presentation/response/LLMSubmitResponse.java b/src/main/java/notai/llm/presentation/response/LLMSubmitResponse.java deleted file mode 100644 index f63240e..0000000 --- a/src/main/java/notai/llm/presentation/response/LLMSubmitResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package notai.llm.presentation.response; - -import notai.llm.application.result.LLMSubmitResult; - -import java.time.LocalDateTime; - -public record LLMSubmitResponse( - Long documentId, - LocalDateTime createdAt -) { - public static LLMSubmitResponse from(LLMSubmitResult result) { - return new LLMSubmitResponse(result.documentId(), result.createdAt()); - } -} diff --git a/src/main/java/notai/llm/presentation/response/LLMResultsResponse.java b/src/main/java/notai/llm/presentation/response/LlmTaskAllPagesResultResponse.java similarity index 58% rename from src/main/java/notai/llm/presentation/response/LLMResultsResponse.java rename to src/main/java/notai/llm/presentation/response/LlmTaskAllPagesResultResponse.java index 4b7a688..83452d3 100644 --- a/src/main/java/notai/llm/presentation/response/LLMResultsResponse.java +++ b/src/main/java/notai/llm/presentation/response/LlmTaskAllPagesResultResponse.java @@ -1,18 +1,18 @@ package notai.llm.presentation.response; -import notai.llm.application.result.LLMResultsResult; -import notai.llm.application.result.LLMResultsResult.LLMContent; -import notai.llm.application.result.LLMResultsResult.LLMResult; +import notai.llm.application.result.LlmTaskAllPagesResult; +import notai.llm.application.result.LlmTaskAllPagesResult.LlmContent; +import notai.llm.application.result.LlmTaskAllPagesResult.LlmResult; import java.util.List; -public record LLMResultsResponse( +public record LlmTaskAllPagesResultResponse( Long documentId, Integer totalPages, List results ) { - public static LLMResultsResponse from(LLMResultsResult result) { - return new LLMResultsResponse( + public static LlmTaskAllPagesResultResponse from(LlmTaskAllPagesResult result) { + return new LlmTaskAllPagesResultResponse( result.documentId(), result.results().size(), result.results().stream().map(Result::from).toList() @@ -23,7 +23,7 @@ public record Result( Integer pageNumber, Content content ) { - public static Result from(LLMResult result) { + public static Result from(LlmResult result) { return new Result(result.pageNumber(), Content.from(result.content())); } } @@ -32,7 +32,7 @@ public record Content( String summary, String problem ) { - public static Content from(LLMContent result) { + public static Content from(LlmContent result) { return new Content(result.summary(), result.problem()); } } diff --git a/src/main/java/notai/llm/presentation/response/LLMStatusResponse.java b/src/main/java/notai/llm/presentation/response/LlmTaskOverallStatusResponse.java similarity index 60% rename from src/main/java/notai/llm/presentation/response/LLMStatusResponse.java rename to src/main/java/notai/llm/presentation/response/LlmTaskOverallStatusResponse.java index 9981105..2e88e8d 100644 --- a/src/main/java/notai/llm/presentation/response/LLMStatusResponse.java +++ b/src/main/java/notai/llm/presentation/response/LlmTaskOverallStatusResponse.java @@ -1,16 +1,16 @@ package notai.llm.presentation.response; -import notai.llm.application.result.LLMStatusResult; +import notai.llm.application.result.LlmTaskOverallStatusResult; import notai.llm.domain.TaskStatus; -public record LLMStatusResponse( +public record LlmTaskOverallStatusResponse( Long documentId, TaskStatus overallStatus, Integer totalPages, Integer completedPages ) { - public static LLMStatusResponse from(LLMStatusResult result) { - return new LLMStatusResponse( + public static LlmTaskOverallStatusResponse from(LlmTaskOverallStatusResult result) { + return new LlmTaskOverallStatusResponse( result.documentId(), result.overallStatus(), result.totalPages(), diff --git a/src/main/java/notai/llm/presentation/response/LlmTaskPageResultResponse.java b/src/main/java/notai/llm/presentation/response/LlmTaskPageResultResponse.java new file mode 100644 index 0000000..8a1bca7 --- /dev/null +++ b/src/main/java/notai/llm/presentation/response/LlmTaskPageResultResponse.java @@ -0,0 +1,15 @@ +package notai.llm.presentation.response; + +import notai.llm.application.result.LlmTaskPageResult; + +public record LlmTaskPageResultResponse( + String summary, + String problem +) { + public static LlmTaskPageResultResponse from(LlmTaskPageResult result) { + return new LlmTaskPageResultResponse( + result.summary(), + result.problem() + ); + } +} diff --git a/src/main/java/notai/llm/presentation/response/LlmTaskPageStatusResponse.java b/src/main/java/notai/llm/presentation/response/LlmTaskPageStatusResponse.java new file mode 100644 index 0000000..faf1d69 --- /dev/null +++ b/src/main/java/notai/llm/presentation/response/LlmTaskPageStatusResponse.java @@ -0,0 +1,12 @@ +package notai.llm.presentation.response; + +import notai.llm.application.result.LlmTaskPageStatusResult; +import notai.llm.domain.TaskStatus; + +public record LlmTaskPageStatusResponse( + TaskStatus status +) { + public static LlmTaskPageStatusResponse from(LlmTaskPageStatusResult result) { + return new LlmTaskPageStatusResponse(result.status()); + } +} diff --git a/src/main/java/notai/llm/presentation/response/LlmTaskSubmitResponse.java b/src/main/java/notai/llm/presentation/response/LlmTaskSubmitResponse.java new file mode 100644 index 0000000..404818d --- /dev/null +++ b/src/main/java/notai/llm/presentation/response/LlmTaskSubmitResponse.java @@ -0,0 +1,14 @@ +package notai.llm.presentation.response; + +import notai.llm.application.result.LlmTaskSubmitResult; + +import java.time.LocalDateTime; + +public record LlmTaskSubmitResponse( + Long documentId, + LocalDateTime createdAt +) { + public static LlmTaskSubmitResponse from(LlmTaskSubmitResult result) { + return new LlmTaskSubmitResponse(result.documentId(), result.createdAt()); + } +} diff --git a/src/main/java/notai/llm/query/LLMQueryRepository.java b/src/main/java/notai/llm/query/LlmTaskQueryRepository.java similarity index 62% rename from src/main/java/notai/llm/query/LLMQueryRepository.java rename to src/main/java/notai/llm/query/LlmTaskQueryRepository.java index d7c1bb0..66e60d9 100644 --- a/src/main/java/notai/llm/query/LLMQueryRepository.java +++ b/src/main/java/notai/llm/query/LlmTaskQueryRepository.java @@ -2,23 +2,23 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; -import notai.llm.domain.QLLM; +import notai.llm.domain.QLlmTask; import notai.llm.domain.TaskStatus; import org.springframework.stereotype.Repository; @Repository @RequiredArgsConstructor -public class LLMQueryRepository { +public class LlmTaskQueryRepository { private final JPAQueryFactory queryFactory; public TaskStatus getTaskStatusBySummaryId(Long summaryId) { - QLLM lLM = QLLM.lLM; + QLlmTask llmTask = QLlmTask.llmTask; return queryFactory - .select(lLM.status) - .from(lLM) - .where(lLM.summary.id.eq(summaryId)) + .select(llmTask.status) + .from(llmTask) + .where(llmTask.summary.id.eq(summaryId)) .fetchOne(); } } diff --git a/src/main/java/notai/member/domain/Member.java b/src/main/java/notai/member/domain/Member.java index 90cb75a..ac08e15 100644 --- a/src/main/java/notai/member/domain/Member.java +++ b/src/main/java/notai/member/domain/Member.java @@ -32,7 +32,6 @@ public class Member extends RootEntity { @Column(length = 20) private String nickname; - @NotNull @Column(length = 255) private String refreshToken; diff --git a/src/main/java/notai/member/presentation/MemberController.java b/src/main/java/notai/member/presentation/MemberController.java index 770b655..bc56652 100644 --- a/src/main/java/notai/member/presentation/MemberController.java +++ b/src/main/java/notai/member/presentation/MemberController.java @@ -14,6 +14,7 @@ import notai.member.presentation.response.MemberFindResponse; import notai.member.presentation.response.MemberOauthLoginResponse; import notai.member.presentation.response.MemberTokenRefreshResponse; +import static org.springframework.http.HttpStatus.CREATED; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -34,7 +35,7 @@ public ResponseEntity loginWithOauth( Member member = oauthClient.fetchMember(oauthProvider, request.oauthAccessToken()); Long memberId = memberService.login(member); TokenPair tokenPair = tokenService.createTokenPair(memberId); - return ResponseEntity.ok(MemberOauthLoginResponse.from(tokenPair)); + return ResponseEntity.status(CREATED).body(MemberOauthLoginResponse.from(tokenPair)); } @PostMapping("/token/refresh") @@ -42,7 +43,7 @@ public ResponseEntity refreshToken( @RequestBody TokenRefreshRequest request ) { TokenPair tokenPair = tokenService.refreshTokenPair(request.refreshToken()); - return ResponseEntity.ok(MemberTokenRefreshResponse.from(tokenPair)); + return ResponseEntity.status(CREATED).body(MemberTokenRefreshResponse.from(tokenPair)); } @GetMapping("/me") diff --git a/src/main/java/notai/ocr/application/OCRQueryService.java b/src/main/java/notai/ocr/application/OCRQueryService.java new file mode 100644 index 0000000..c0b4c87 --- /dev/null +++ b/src/main/java/notai/ocr/application/OCRQueryService.java @@ -0,0 +1,24 @@ +package notai.ocr.application; + +import lombok.RequiredArgsConstructor; +import static notai.common.exception.ErrorMessages.OCR_RESULT_NOT_FOUND; +import notai.common.exception.type.NotFoundException; +import notai.ocr.application.result.OCRFindResult; +import notai.ocr.domain.OCR; +import notai.ocr.domain.OCRRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OCRQueryService { + + private final OCRRepository ocrRepository; + + public OCRFindResult findOCR(Long documentId, Integer pageNumber) { + OCR ocr = ocrRepository.findOCRByDocumentIdAndPageNumber(documentId, + pageNumber + ).orElseThrow(() -> new NotFoundException(OCR_RESULT_NOT_FOUND)); + + return OCRFindResult.of(documentId, pageNumber, ocr.getContent()); + } +} diff --git a/src/main/java/notai/ocr/application/OCRService.java b/src/main/java/notai/ocr/application/OCRService.java index 42cc619..55353ba 100644 --- a/src/main/java/notai/ocr/application/OCRService.java +++ b/src/main/java/notai/ocr/application/OCRService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import net.sourceforge.tess4j.Tesseract; +import static notai.common.exception.ErrorMessages.OCR_TASK_ERROR; import notai.common.exception.type.FileProcessException; import notai.document.domain.Document; import notai.ocr.domain.OCR; @@ -15,8 +16,6 @@ import java.awt.image.BufferedImage; import java.io.File; -import static notai.common.exception.ErrorMessages.OCR_TASK_ERROR; - @Service @RequiredArgsConstructor public class OCRService { @@ -28,11 +27,14 @@ public void saveOCR( Document document, File pdfFile ) { try { - System.setProperty("jna.library.path", "/usr/local/opt/tesseract/lib/"); + // System.setProperty("jna.library.path", "/usr/local/opt/tesseract/lib/"); + System.setProperty("jna.library.path", "C:\\Program Files\\Tesseract-OCR"); + //window, mac -> brew install tesseract, tesseract-lang Tesseract tesseract = new Tesseract(); - tesseract.setDatapath("/usr/local/share/tessdata"); + // tesseract.setDatapath("/usr/local/share/tessdata"); + tesseract.setDatapath("C:\\Program Files\\Tesseract-OCR\\tessdata"); tesseract.setLanguage("kor+eng"); PDDocument pdDocument = Loader.loadPDF(pdfFile); @@ -49,4 +51,8 @@ public void saveOCR( throw new FileProcessException(OCR_TASK_ERROR); } } + + public void deleteAllByDocument(Document document) { + ocrRepository.deleteAllByDocument(document); + } } diff --git a/src/main/java/notai/ocr/application/result/OCRFindResult.java b/src/main/java/notai/ocr/application/result/OCRFindResult.java new file mode 100644 index 0000000..909f183 --- /dev/null +++ b/src/main/java/notai/ocr/application/result/OCRFindResult.java @@ -0,0 +1,11 @@ +package notai.ocr.application.result; + +public record OCRFindResult( + Long documentId, + Integer pageNumber, + String result +) { + public static OCRFindResult of(Long documentId, Integer pageNumber, String result) { + return new OCRFindResult(documentId, pageNumber, result); + } +} diff --git a/src/main/java/notai/ocr/domain/OCRRepository.java b/src/main/java/notai/ocr/domain/OCRRepository.java index 18da4e8..4211e79 100644 --- a/src/main/java/notai/ocr/domain/OCRRepository.java +++ b/src/main/java/notai/ocr/domain/OCRRepository.java @@ -1,19 +1,18 @@ package notai.ocr.domain; +import static notai.common.exception.ErrorMessages.OCR_RESULT_NOT_FOUND; import notai.common.exception.type.NotFoundException; import notai.document.domain.Document; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -import static notai.common.exception.ErrorMessages.OCR_RESULT_NOT_FOUND; +import java.util.Optional; public interface OCRRepository extends JpaRepository { default OCR getById(Long id) { return findById(id).orElseThrow(() -> new NotFoundException(OCR_RESULT_NOT_FOUND)); } - List findAllByDocumentId(Long documentId); + Optional findOCRByDocumentIdAndPageNumber(Long documentId, Integer pageNumber); void deleteAllByDocument(Document document); } diff --git a/src/main/java/notai/ocr/presentation/OCRController.java b/src/main/java/notai/ocr/presentation/OCRController.java new file mode 100644 index 0000000..e042773 --- /dev/null +++ b/src/main/java/notai/ocr/presentation/OCRController.java @@ -0,0 +1,26 @@ +package notai.ocr.presentation; + +import lombok.RequiredArgsConstructor; +import notai.ocr.application.OCRQueryService; +import notai.ocr.application.result.OCRFindResult; +import notai.ocr.presentation.response.OCRFindResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/documents/{documentId}/ocrs") +@RequiredArgsConstructor +public class OCRController { + + private final OCRQueryService ocrQueryService; + + @GetMapping + public ResponseEntity getDocuments( + @PathVariable Long documentId, @RequestParam Integer pageNumber + ) { + OCRFindResult result = ocrQueryService.findOCR(documentId, pageNumber); + OCRFindResponse response = OCRFindResponse.from(result); + return ResponseEntity.ok(response); + } +} + diff --git a/src/main/java/notai/ocr/presentation/response/OCRFindResponse.java b/src/main/java/notai/ocr/presentation/response/OCRFindResponse.java new file mode 100644 index 0000000..78efc86 --- /dev/null +++ b/src/main/java/notai/ocr/presentation/response/OCRFindResponse.java @@ -0,0 +1,15 @@ +package notai.ocr.presentation.response; + +import notai.ocr.application.result.OCRFindResult; + +public record OCRFindResponse( + Long documentId, + Integer pageNumber, + String result +) { + public static OCRFindResponse from( + OCRFindResult ocrFindResult + ) { + return new OCRFindResponse(ocrFindResult.documentId(), ocrFindResult.pageNumber(), ocrFindResult.result()); + } +} diff --git a/src/main/java/notai/pageRecording/presentation/PageRecordingController.java b/src/main/java/notai/pageRecording/presentation/PageRecordingController.java index a5cfc4c..9506f7f 100644 --- a/src/main/java/notai/pageRecording/presentation/PageRecordingController.java +++ b/src/main/java/notai/pageRecording/presentation/PageRecordingController.java @@ -1,11 +1,17 @@ package notai.pageRecording.presentation; +import static org.springframework.http.HttpStatus.CREATED; + import lombok.RequiredArgsConstructor; import notai.pageRecording.application.PageRecordingService; import notai.pageRecording.application.command.PageRecordingSaveCommand; import notai.pageRecording.presentation.request.PageRecordingSaveRequest; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/documents/{documentId}/recordings/page-turns") @@ -20,6 +26,6 @@ public ResponseEntity savePageRecording( ) { PageRecordingSaveCommand command = request.toCommand(documentId); pageRecordingService.savePageRecording(command); - return ResponseEntity.ok().build(); + return ResponseEntity.status(CREATED).build(); } } diff --git a/src/main/java/notai/pdf/PdfController.java b/src/main/java/notai/pdf/PdfController.java index 9d58c78..b68a1b9 100644 --- a/src/main/java/notai/pdf/PdfController.java +++ b/src/main/java/notai/pdf/PdfController.java @@ -5,14 +5,14 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.io.File; -@Controller +@RestController @RequestMapping("/pdf") @RequiredArgsConstructor public class PdfController { diff --git a/src/main/java/notai/pdf/PdfService.java b/src/main/java/notai/pdf/PdfService.java index c07a2e0..f427ec7 100644 --- a/src/main/java/notai/pdf/PdfService.java +++ b/src/main/java/notai/pdf/PdfService.java @@ -1,9 +1,14 @@ package notai.pdf; import lombok.RequiredArgsConstructor; +import notai.common.exception.ErrorMessages; +import static notai.common.exception.ErrorMessages.FILE_NOT_FOUND; +import static notai.common.exception.ErrorMessages.FILE_SAVE_ERROR; import notai.common.exception.type.FileProcessException; import notai.common.exception.type.NotFoundException; import notai.pdf.result.PdfSaveResult; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -14,9 +19,6 @@ import java.nio.file.Paths; import java.util.UUID; -import static notai.common.exception.ErrorMessages.FILE_NOT_FOUND; -import static notai.common.exception.ErrorMessages.FILE_SAVE_ERROR; - @Service @RequiredArgsConstructor public class PdfService { @@ -32,9 +34,10 @@ public PdfSaveResult savePdf(MultipartFile file) { String fileName = UUID.randomUUID() + ".pdf"; Path filePath = directoryPath.resolve(fileName); - file.transferTo(filePath.toFile()); - - return PdfSaveResult.of(fileName, filePath.toFile()); + File pdfFile = filePath.toFile(); + file.transferTo(pdfFile); + Integer totalPages = getTotalPages(pdfFile); + return PdfSaveResult.of(fileName, pdfFile, totalPages); } catch (IOException exception) { throw new FileProcessException(FILE_SAVE_ERROR); } @@ -48,4 +51,15 @@ public File getPdf(String fileName) { } return filePath.toFile(); } + + private Integer getTotalPages(File file) { + try { + PDDocument document = Loader.loadPDF(file); + Integer totalPages = document.getNumberOfPages(); + document.close(); + return totalPages; + } catch (IOException e) { + throw new FileProcessException(ErrorMessages.INVALID_FILE_TYPE); + } + } } diff --git a/src/main/java/notai/pdf/result/PdfSaveResult.java b/src/main/java/notai/pdf/result/PdfSaveResult.java index 340d4af..39eb01d 100644 --- a/src/main/java/notai/pdf/result/PdfSaveResult.java +++ b/src/main/java/notai/pdf/result/PdfSaveResult.java @@ -5,12 +5,13 @@ public record PdfSaveResult( String pdfName, String pdfUrl, - File pdf + File pdf, + Integer totalPages ) { public static PdfSaveResult of( - String pdfName, File pdf + String pdfName, File pdf, Integer totalPages ) { - return new PdfSaveResult(pdfName, convertPdfUrl(pdfName), pdf); + return new PdfSaveResult(pdfName, convertPdfUrl(pdfName), pdf, totalPages); } private static String convertPdfUrl(String pdfName) { diff --git a/src/main/java/notai/post/application/PostQueryService.java b/src/main/java/notai/post/application/PostQueryService.java index 1d33de5..1c0ccea 100644 --- a/src/main/java/notai/post/application/PostQueryService.java +++ b/src/main/java/notai/post/application/PostQueryService.java @@ -1,4 +1,9 @@ package notai.post.application; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor public class PostQueryService { } diff --git a/src/main/java/notai/post/application/PostService.java b/src/main/java/notai/post/application/PostService.java index be1c63e..eee4f3f 100644 --- a/src/main/java/notai/post/application/PostService.java +++ b/src/main/java/notai/post/application/PostService.java @@ -1,4 +1,30 @@ package notai.post.application; +import lombok.RequiredArgsConstructor; +import notai.member.domain.Member; +import notai.member.domain.MemberRepository; +import notai.post.application.command.PostSaveCommand; +import notai.post.application.result.PostFindResult; +import notai.post.application.result.PostSaveResult; +import notai.post.domain.Post; +import notai.post.domain.PostRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor public class PostService { + private final PostRepository postRepository; + private final MemberRepository memberRepository; + + public PostSaveResult savePost(PostSaveCommand postSaveCommand) { + Member member = memberRepository.getById(postSaveCommand.memberId()); + Post post = new Post(member, postSaveCommand.title(), postSaveCommand.content()); + Post savedPost = postRepository.save(post); + return PostSaveResult.of(savedPost.getId(), savedPost.getTitle()); + } + + public PostFindResult findPost(Long postId) { + Post post = postRepository.findById(postId).get(); + return PostFindResult.of(post); + } } diff --git a/src/main/java/notai/post/application/command/PostSaveCommand.java b/src/main/java/notai/post/application/command/PostSaveCommand.java new file mode 100644 index 0000000..bcbfdf6 --- /dev/null +++ b/src/main/java/notai/post/application/command/PostSaveCommand.java @@ -0,0 +1,8 @@ +package notai.post.application.command; + +public record PostSaveCommand( + Long memberId, + String title, + String content +) { +} diff --git a/src/main/java/notai/post/application/result/PostFindResult.java b/src/main/java/notai/post/application/result/PostFindResult.java new file mode 100644 index 0000000..61ed4ad --- /dev/null +++ b/src/main/java/notai/post/application/result/PostFindResult.java @@ -0,0 +1,28 @@ +package notai.post.application.result; + +import notai.post.domain.Post; + +import java.time.LocalDateTime; + +public record PostFindResult( + Long id, + Long memberId, + String title, + String contents, + LocalDateTime createdAt, + LocalDateTime updatedAt + +) { + public static PostFindResult of( + Post post + ) { + return new PostFindResult( + post.getId(), + post.getMember().getId(), + post.getTitle(), + post.getContents(), + post.getCreatedAt(), + post.getUpdatedAt() + ); + } +} diff --git a/src/main/java/notai/post/application/result/PostSaveResult.java b/src/main/java/notai/post/application/result/PostSaveResult.java new file mode 100644 index 0000000..939cdc3 --- /dev/null +++ b/src/main/java/notai/post/application/result/PostSaveResult.java @@ -0,0 +1,10 @@ +package notai.post.application.result; + +public record PostSaveResult( + Long id, + String title +) { + public static PostSaveResult of(Long id, String title) { + return new PostSaveResult(id, title); + } +} diff --git a/src/main/java/notai/post/domain/Post.java b/src/main/java/notai/post/domain/Post.java index 44e18b1..3c9a999 100644 --- a/src/main/java/notai/post/domain/Post.java +++ b/src/main/java/notai/post/domain/Post.java @@ -1,21 +1,21 @@ package notai.post.domain; import jakarta.persistence.*; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; import jakarta.validation.constraints.NotNull; +import static lombok.AccessLevel.PROTECTED; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import notai.common.domain.RootEntity; import notai.member.domain.Member; -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - @Getter @NoArgsConstructor(access = PROTECTED) @AllArgsConstructor @Entity -public class Post { +public class Post extends RootEntity { @Id @GeneratedValue(strategy = IDENTITY) diff --git a/src/main/java/notai/post/presentation/PostController.java b/src/main/java/notai/post/presentation/PostController.java index c13b067..c28b077 100644 --- a/src/main/java/notai/post/presentation/PostController.java +++ b/src/main/java/notai/post/presentation/PostController.java @@ -1,4 +1,41 @@ package notai.post.presentation; +import lombok.RequiredArgsConstructor; +import notai.post.application.command.PostSaveCommand; +import notai.post.application.PostService; +import notai.post.application.result.PostFindResult; +import notai.post.application.result.PostSaveResult; +import notai.post.presentation.request.PostSaveRequest; +import notai.post.presentation.response.PostFindResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@RestController +@RequestMapping("/api/post") +@RequiredArgsConstructor public class PostController { + + private final PostService postService; + + @PostMapping + public ResponseEntity savePost( + @RequestBody PostSaveRequest postSaveRequest + ) { + PostSaveCommand postSaveCommand = postSaveRequest.toCommand(); + PostSaveResult postSaveResult = postService.savePost(postSaveCommand); + String url = String.format("/api/post/%s", postSaveResult.id()); + return ResponseEntity.created(URI.create(url)).build(); + } + + @GetMapping(value = "/{postId}") + public ResponseEntity getPost( + @PathVariable Long postId + ) { + PostFindResult postFindResult = postService.findPost(postId); + PostFindResponse response = PostFindResponse.from(postFindResult); + return ResponseEntity.ok(response); + } + } diff --git a/src/main/java/notai/post/presentation/request/PostSaveRequest.java b/src/main/java/notai/post/presentation/request/PostSaveRequest.java new file mode 100644 index 0000000..82d3863 --- /dev/null +++ b/src/main/java/notai/post/presentation/request/PostSaveRequest.java @@ -0,0 +1,13 @@ +package notai.post.presentation.request; + +import notai.post.application.command.PostSaveCommand; + +public record PostSaveRequest( + Long memberId, + String title, + String content +) { + public PostSaveCommand toCommand() { + return new PostSaveCommand(memberId,title,content); + } +} diff --git a/src/main/java/notai/post/presentation/response/PostFindResponse.java b/src/main/java/notai/post/presentation/response/PostFindResponse.java new file mode 100644 index 0000000..c9c754c --- /dev/null +++ b/src/main/java/notai/post/presentation/response/PostFindResponse.java @@ -0,0 +1,25 @@ +package notai.post.presentation.response; + +import notai.post.application.result.PostFindResult; + +import java.time.LocalDateTime; + +public record PostFindResponse( + Long id, + Long memberId, + String title, + String contents, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static PostFindResponse from(PostFindResult postFindResult) { + return new PostFindResponse( + postFindResult.id(), + postFindResult.memberId(), + postFindResult.title(), + postFindResult.contents(), + postFindResult.createdAt(), + postFindResult.updatedAt() + ); + } +} diff --git a/src/main/java/notai/post/query/PostQueryRepository.java b/src/main/java/notai/post/query/PostQueryRepository.java new file mode 100644 index 0000000..817bd44 --- /dev/null +++ b/src/main/java/notai/post/query/PostQueryRepository.java @@ -0,0 +1,7 @@ +package notai.post.query; + +import org.springframework.stereotype.Repository; + +@Repository +public interface PostQueryRepository { +} diff --git a/src/main/java/notai/problem/domain/Problem.java b/src/main/java/notai/problem/domain/Problem.java index 0b28a63..a6118ed 100644 --- a/src/main/java/notai/problem/domain/Problem.java +++ b/src/main/java/notai/problem/domain/Problem.java @@ -14,6 +14,7 @@ @Getter @NoArgsConstructor(access = PROTECTED) @Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "document_id", "page_number" }) }) public class Problem extends RootEntity { @Id diff --git a/src/main/java/notai/problem/domain/ProblemRepository.java b/src/main/java/notai/problem/domain/ProblemRepository.java index dcfc221..3b011e3 100644 --- a/src/main/java/notai/problem/domain/ProblemRepository.java +++ b/src/main/java/notai/problem/domain/ProblemRepository.java @@ -1,12 +1,17 @@ package notai.problem.domain; +import static notai.common.exception.ErrorMessages.PROBLEM_NOT_FOUND; + +import java.util.Optional; import notai.common.exception.type.NotFoundException; +import notai.document.domain.Document; +import notai.problem.query.ProblemQueryRepository; import org.springframework.data.jpa.repository.JpaRepository; -import static notai.common.exception.ErrorMessages.PROBLEM_NOT_FOUND; - -public interface ProblemRepository extends JpaRepository { +public interface ProblemRepository extends JpaRepository, ProblemQueryRepository { default Problem getById(Long id) { return findById(id).orElseThrow(() -> new NotFoundException(PROBLEM_NOT_FOUND)); } + + Optional findByDocumentAndPageNumber(Document document, Integer pageNumber); } diff --git a/src/main/java/notai/problem/query/ProblemQueryRepository.java b/src/main/java/notai/problem/query/ProblemQueryRepository.java index e1a51f0..c9abfc6 100644 --- a/src/main/java/notai/problem/query/ProblemQueryRepository.java +++ b/src/main/java/notai/problem/query/ProblemQueryRepository.java @@ -1,41 +1,11 @@ package notai.problem.query; -import com.querydsl.core.types.Projections; -import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; -import notai.problem.domain.QProblem; -import notai.problem.query.result.ProblemPageContentResult; -import org.springframework.stereotype.Repository; - import java.util.List; +import notai.problem.query.result.ProblemPageContentResult; -@Repository -@RequiredArgsConstructor -public class ProblemQueryRepository { - - private final JPAQueryFactory queryFactory; - - public List getProblemIdsByDocumentId(Long documentId) { - QProblem problem = QProblem.problem; - - return queryFactory - .select(problem.id) - .from(problem) - .where(problem.document.id.eq(documentId)) - .fetch(); - } +public interface ProblemQueryRepository { - public List getPageNumbersAndContentByDocumentId(Long documentId) { - QProblem problem = QProblem.problem; + List getPageNumbersAndContentByDocumentId(Long documentId); - return queryFactory - .select(Projections.constructor( - ProblemPageContentResult.class, - problem.pageNumber, - problem.content - )) - .from(problem) - .where(problem.document.id.eq(documentId).and(problem.content.isNotNull())) - .fetch(); - } + String getProblemContentByDocumentIdAndPageNumber(Long documentId, Integer pageNumber); } diff --git a/src/main/java/notai/problem/query/ProblemQueryRepositoryImpl.java b/src/main/java/notai/problem/query/ProblemQueryRepositoryImpl.java new file mode 100644 index 0000000..597bb40 --- /dev/null +++ b/src/main/java/notai/problem/query/ProblemQueryRepositoryImpl.java @@ -0,0 +1,40 @@ +package notai.problem.query; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import notai.problem.domain.QProblem; +import notai.problem.query.result.ProblemPageContentResult; + +@RequiredArgsConstructor +public class ProblemQueryRepositoryImpl implements ProblemQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List getPageNumbersAndContentByDocumentId(Long documentId) { + QProblem problem = QProblem.problem; + + return queryFactory + .select(Projections.constructor( + ProblemPageContentResult.class, + problem.pageNumber, + problem.content + )) + .from(problem) + .where(problem.document.id.eq(documentId).and(problem.content.isNotNull())) + .fetch(); + } + + @Override + public String getProblemContentByDocumentIdAndPageNumber(Long documentId, Integer pageNumber) { + QProblem problem = QProblem.problem; + + return queryFactory + .select(problem.content) + .from(problem) + .where(problem.document.id.eq(documentId).and(problem.pageNumber.eq(pageNumber))) + .fetchOne(); + } +} diff --git a/src/main/java/notai/recording/presentation/RecordingController.java b/src/main/java/notai/recording/presentation/RecordingController.java index 92e5de5..9224cd4 100644 --- a/src/main/java/notai/recording/presentation/RecordingController.java +++ b/src/main/java/notai/recording/presentation/RecordingController.java @@ -1,5 +1,7 @@ package notai.recording.presentation; +import static org.springframework.http.HttpStatus.CREATED; + import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import notai.recording.application.RecordingService; @@ -8,7 +10,11 @@ import notai.recording.presentation.request.RecordingSaveRequest; import notai.recording.presentation.response.RecordingSaveResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/documents/{documentId}/recordings") @@ -23,6 +29,6 @@ public ResponseEntity saveRecording( ) { RecordingSaveCommand command = request.toCommand(documentId); RecordingSaveResult result = recordingService.saveRecording(command); - return ResponseEntity.ok(RecordingSaveResponse.from(result)); + return ResponseEntity.status(CREATED).body(RecordingSaveResponse.from(result)); } } diff --git a/src/main/java/notai/summary/domain/Summary.java b/src/main/java/notai/summary/domain/Summary.java index 9882179..f2b080d 100644 --- a/src/main/java/notai/summary/domain/Summary.java +++ b/src/main/java/notai/summary/domain/Summary.java @@ -14,6 +14,7 @@ @Getter @NoArgsConstructor(access = PROTECTED) @Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "document_id", "page_number" }) }) public class Summary extends RootEntity { @Id diff --git a/src/main/java/notai/summary/domain/SummaryRepository.java b/src/main/java/notai/summary/domain/SummaryRepository.java index 87f661f..5b216e4 100644 --- a/src/main/java/notai/summary/domain/SummaryRepository.java +++ b/src/main/java/notai/summary/domain/SummaryRepository.java @@ -1,12 +1,17 @@ package notai.summary.domain; +import static notai.common.exception.ErrorMessages.SUMMARY_NOT_FOUND; + +import java.util.Optional; import notai.common.exception.type.NotFoundException; +import notai.document.domain.Document; +import notai.summary.query.SummaryQueryRepository; import org.springframework.data.jpa.repository.JpaRepository; -import static notai.common.exception.ErrorMessages.SUMMARY_NOT_FOUND; - -public interface SummaryRepository extends JpaRepository { +public interface SummaryRepository extends JpaRepository, SummaryQueryRepository { default Summary getById(Long id) { return findById(id).orElseThrow(() -> new NotFoundException(SUMMARY_NOT_FOUND)); } + + Optional findByDocumentAndPageNumber(Document document, int pageNumber); } diff --git a/src/main/java/notai/summary/query/SummaryQueryRepository.java b/src/main/java/notai/summary/query/SummaryQueryRepository.java index e522293..5669a61 100644 --- a/src/main/java/notai/summary/query/SummaryQueryRepository.java +++ b/src/main/java/notai/summary/query/SummaryQueryRepository.java @@ -1,41 +1,15 @@ package notai.summary.query; -import com.querydsl.core.types.Projections; -import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; -import notai.summary.domain.QSummary; -import notai.summary.query.result.SummaryPageContentResult; -import org.springframework.stereotype.Repository; - import java.util.List; +import notai.summary.query.result.SummaryPageContentResult; -@Repository -@RequiredArgsConstructor -public class SummaryQueryRepository { - - private final JPAQueryFactory queryFactory; +public interface SummaryQueryRepository { - public List getSummaryIdsByDocumentId(Long documentId) { - QSummary summary = QSummary.summary; + List getSummaryIdsByDocumentId(Long documentId); - return queryFactory - .select(summary.id) - .from(summary) - .where(summary.document.id.eq(documentId)) - .fetch(); - } + List getPageNumbersAndContentByDocumentId(Long documentId); - public List getPageNumbersAndContentByDocumentId(Long documentId) { - QSummary summary = QSummary.summary; + Long getSummaryIdByDocumentIdAndPageNumber(Long documentId, Integer pageNumber); - return queryFactory - .select(Projections.constructor( - SummaryPageContentResult.class, - summary.pageNumber, - summary.content - )) - .from(summary) - .where(summary.document.id.eq(documentId).and(summary.content.isNotNull())) - .fetch(); - } + String getSummaryContentByDocumentIdAndPageNumber(Long documentId, Integer pageNumber); } diff --git a/src/main/java/notai/summary/query/SummaryQueryRepositoryImpl.java b/src/main/java/notai/summary/query/SummaryQueryRepositoryImpl.java new file mode 100644 index 0000000..72d1ac0 --- /dev/null +++ b/src/main/java/notai/summary/query/SummaryQueryRepositoryImpl.java @@ -0,0 +1,62 @@ +package notai.summary.query; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import notai.summary.domain.QSummary; +import notai.summary.query.result.SummaryPageContentResult; + +@RequiredArgsConstructor +public class SummaryQueryRepositoryImpl implements SummaryQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List getSummaryIdsByDocumentId(Long documentId) { + QSummary summary = QSummary.summary; + + return queryFactory + .select(summary.id) + .from(summary) + .where(summary.document.id.eq(documentId)) + .fetch(); + } + + @Override + public List getPageNumbersAndContentByDocumentId(Long documentId) { + QSummary summary = QSummary.summary; + + return queryFactory + .select(Projections.constructor( + SummaryPageContentResult.class, + summary.pageNumber, + summary.content + )) + .from(summary) + .where(summary.document.id.eq(documentId).and(summary.content.isNotNull())) + .fetch(); + } + + @Override + public Long getSummaryIdByDocumentIdAndPageNumber(Long documentId, Integer pageNumber) { + QSummary summary = QSummary.summary; + + return queryFactory + .select(summary.id) + .from(summary) + .where(summary.document.id.eq(documentId).and(summary.pageNumber.eq(pageNumber))) + .fetchOne(); + } + + @Override + public String getSummaryContentByDocumentIdAndPageNumber(Long documentId, Integer pageNumber) { + QSummary summary = QSummary.summary; + + return queryFactory + .select(summary.content) + .from(summary) + .where(summary.document.id.eq(documentId).and(summary.pageNumber.eq(pageNumber))) + .fetchOne(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 79a97f3..3baccb8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,4 +4,4 @@ spring: file: audio: - basePath: src/main/resources/audio/ \ No newline at end of file + basePath: /app/audio/ # Docker 볼륨에 맞게 경로 수정 \ No newline at end of file diff --git a/src/test/java/notai/document/application/DocumentServiceTest.java b/src/test/java/notai/document/application/DocumentServiceTest.java deleted file mode 100644 index 6b82bfd..0000000 --- a/src/test/java/notai/document/application/DocumentServiceTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package notai.document.application; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class DocumentServiceTest { - - @InjectMocks - PdfService pdfService; - -} diff --git a/src/test/java/notai/document/application/PdfServiceTest.java b/src/test/java/notai/document/application/PdfServiceTest.java deleted file mode 100644 index 70f2397..0000000 --- a/src/test/java/notai/document/application/PdfServiceTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package notai.document.application; - -import net.sourceforge.tess4j.Tesseract; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.rendering.PDFRenderer; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.core.io.ClassPathResource; -import org.springframework.mock.web.MockMultipartFile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -@ExtendWith(MockitoExtension.class) -class PdfServiceTest { - - @InjectMocks - PdfService pdfService; - - static final String STORAGE_DIR = "src/main/resources/pdf/"; - - @Test - void savePdf_success_existsTestPdf() throws IOException { - //given - ClassPathResource existsPdf = new ClassPathResource("pdf/test.pdf"); - MockMultipartFile mockFile = new MockMultipartFile("file", - existsPdf.getFilename(), - "application/pdf", - Files.readAllBytes(existsPdf.getFile().toPath()) - ); - //when - String savedFileName = pdfService.savePdf(mockFile); - //then - Path savedFilePath = Paths.get(STORAGE_DIR, savedFileName); - Assertions.assertThat(Files.exists(savedFilePath)).isTrue(); - - System.setProperty("jna.library.path", "/usr/local/opt/tesseract/lib/"); - //window, mac -> brew install tesseract, tesseract-lang - Tesseract tesseract = new Tesseract(); - - tesseract.setDatapath("/usr/local/share/tessdata"); - tesseract.setLanguage("kor+eng"); - - try { - PDDocument pdDocument = Loader.loadPDF(savedFilePath.toFile()); - PDFRenderer pdfRenderer = new PDFRenderer(pdDocument); - - var image = pdfRenderer.renderImage(9); - var start = System.currentTimeMillis(); - var ocrResult = tesseract.doOCR(image); - System.out.println("result : " + ocrResult); - var end = System.currentTimeMillis(); - System.out.println(end - start); - pdDocument.close(); - } catch (Exception e) { - e.printStackTrace(); - } - - deleteFile(savedFilePath); - } - - void deleteFile(Path filePath) throws IOException { - if (Files.exists(filePath)) { - Files.delete(filePath); - } - } -} diff --git a/src/test/java/notai/llm/application/LLMQueryServiceTest.java b/src/test/java/notai/llm/application/LLMQueryServiceTest.java deleted file mode 100644 index c1d9372..0000000 --- a/src/test/java/notai/llm/application/LLMQueryServiceTest.java +++ /dev/null @@ -1,165 +0,0 @@ -package notai.llm.application; - -import notai.common.exception.type.InternalServerErrorException; -import notai.common.exception.type.NotFoundException; -import notai.document.domain.DocumentRepository; -import notai.llm.application.result.LLMResultsResult; -import notai.llm.application.result.LLMStatusResult; -import notai.llm.query.LLMQueryRepository; -import notai.problem.query.ProblemQueryRepository; -import notai.problem.query.result.ProblemPageContentResult; -import notai.summary.query.SummaryQueryRepository; -import notai.summary.query.result.SummaryPageContentResult; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; - -import static notai.llm.domain.TaskStatus.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -class LLMQueryServiceTest { - - @InjectMocks - private LLMQueryService llmQueryService; - - @Mock - private LLMQueryRepository llmQueryRepository; - - @Mock - private DocumentRepository documentRepository; - - @Mock - private SummaryQueryRepository summaryQueryRepository; - - @Mock - private ProblemQueryRepository problemQueryRepository; - - @Test - void 작업_상태_확인시_존재하지_않는_문서ID로_요청한_경우_예외_발생() { - // given - given(documentRepository.existsById(anyLong())).willReturn(false); - - // when & then - assertAll(() -> assertThrows(NotFoundException.class, () -> llmQueryService.fetchTaskStatus(1L)), - () -> verify(documentRepository).existsById(anyLong()) - ); - } - - @Test - void 작업_상태_확인시_모든_페이지의_작업이_완료된_경우_COMPLETED() { - // given - Long documentId = 1L; - List summaryIds = List.of(1L, 2L, 3L); - - given(documentRepository.existsById(anyLong())).willReturn(true); - given(summaryQueryRepository.getSummaryIdsByDocumentId(documentId)).willReturn(summaryIds); - given(llmQueryRepository.getTaskStatusBySummaryId(1L)).willReturn(COMPLETED); - given(llmQueryRepository.getTaskStatusBySummaryId(2L)).willReturn(COMPLETED); - given(llmQueryRepository.getTaskStatusBySummaryId(3L)).willReturn(COMPLETED); - - // when - LLMStatusResult result = llmQueryService.fetchTaskStatus(documentId); - - // then - assertAll(() -> assertThat(result.overallStatus()).isEqualTo(COMPLETED), - () -> assertThat(result.totalPages()).isEqualTo(3), - () -> assertThat(result.completedPages()).isEqualTo(3), - () -> verify(documentRepository).existsById(documentId), - () -> verify(summaryQueryRepository).getSummaryIdsByDocumentId(documentId), - () -> verify(llmQueryRepository).getTaskStatusBySummaryId(documentId) - ); - } - - @Test - void 작업_상태_확인시_모든_페이지의_작업이_완료되지_않은_경우_IN_PROGRESS() { - // given - Long documentId = 1L; - List summaryIds = List.of(1L, 2L, 3L); - - given(documentRepository.existsById(anyLong())).willReturn(true); - given(summaryQueryRepository.getSummaryIdsByDocumentId(documentId)).willReturn(summaryIds); - given(llmQueryRepository.getTaskStatusBySummaryId(1L)).willReturn(COMPLETED); - given(llmQueryRepository.getTaskStatusBySummaryId(2L)).willReturn(IN_PROGRESS); - given(llmQueryRepository.getTaskStatusBySummaryId(3L)).willReturn(PENDING); - - // when - LLMStatusResult result = llmQueryService.fetchTaskStatus(documentId); - - // then - assertAll(() -> assertThat(result.overallStatus()).isEqualTo(IN_PROGRESS), - () -> assertThat(result.totalPages()).isEqualTo(3), - () -> assertThat(result.completedPages()).isEqualTo(1), - () -> verify(documentRepository).existsById(documentId), - () -> verify(summaryQueryRepository).getSummaryIdsByDocumentId(documentId), - () -> verify(llmQueryRepository).getTaskStatusBySummaryId(documentId) - ); - } - - @Test - void 작업_결과_확인시_존재하지_않는_문서ID로_요청한_경우_예외_발생() { - // given - given(documentRepository.existsById(anyLong())).willReturn(false); - - // when & then - assertAll(() -> assertThrows(NotFoundException.class, () -> llmQueryService.findTaskResult(1L)), - () -> verify(documentRepository).existsById(anyLong()) - ); - } - - @Test - void 작업_결과_확인시_생성된_요약과_문제의_수가_일치하지_않는_경우_예외_발생() { - // given - Long documentId = 1L; - List summaryResults = List.of(new SummaryPageContentResult(1, "요약 내용")); - List problemResults = List.of(new ProblemPageContentResult(1, "요약 내용"), - new ProblemPageContentResult(2, "요약 내용") - ); - - given(documentRepository.existsById(anyLong())).willReturn(true); - given(summaryQueryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(summaryResults); - given(problemQueryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(problemResults); - - // when & then - assertAll(() -> assertThrows(InternalServerErrorException.class, () -> llmQueryService.findTaskResult(1L)), - () -> verify(documentRepository).existsById(documentId), - () -> verify(summaryQueryRepository).getPageNumbersAndContentByDocumentId(documentId), - () -> verify(problemQueryRepository).getPageNumbersAndContentByDocumentId(documentId) - ); - } - - @Test - void 작업_결과_확인() { - // given - Long documentId = 1L; - List summaryResults = List.of(new SummaryPageContentResult(1, "요약 내용"), - new SummaryPageContentResult(2, "요약 내용") - ); - List problemResults = List.of(new ProblemPageContentResult(1, "요약 내용"), - new ProblemPageContentResult(2, "요약 내용") - ); - - given(documentRepository.existsById(anyLong())).willReturn(true); - given(summaryQueryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(summaryResults); - given(problemQueryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(problemResults); - - // when - LLMResultsResult response = llmQueryService.findTaskResult(documentId); - - // then - assertAll(() -> assertEquals(documentId, response.documentId()), - () -> assertEquals(2, response.results().size()), - () -> verify(documentRepository).existsById(documentId), - () -> verify(summaryQueryRepository).getPageNumbersAndContentByDocumentId(documentId), - () -> verify(problemQueryRepository).getPageNumbersAndContentByDocumentId(documentId) - ); - } -} \ No newline at end of file diff --git a/src/test/java/notai/llm/application/LlmTaskQueryServiceTest.java b/src/test/java/notai/llm/application/LlmTaskQueryServiceTest.java new file mode 100644 index 0000000..390b0bc --- /dev/null +++ b/src/test/java/notai/llm/application/LlmTaskQueryServiceTest.java @@ -0,0 +1,302 @@ +package notai.llm.application; + +import notai.common.exception.type.InternalServerErrorException; +import notai.common.exception.type.NotFoundException; +import notai.document.domain.DocumentRepository; +import notai.llm.application.command.LlmTaskPageResultCommand; +import notai.llm.application.command.LlmTaskPageStatusCommand; +import notai.llm.application.result.LlmTaskAllPagesResult; +import notai.llm.application.result.LlmTaskOverallStatusResult; +import notai.llm.application.result.LlmTaskPageResult; +import notai.llm.application.result.LlmTaskPageStatusResult; +import static notai.llm.domain.TaskStatus.*; +import notai.llm.query.LlmTaskQueryRepository; +import notai.problem.domain.ProblemRepository; +import notai.problem.query.result.ProblemPageContentResult; +import notai.summary.domain.SummaryRepository; +import notai.summary.query.result.SummaryPageContentResult; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.verify; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class LlmTaskQueryServiceTest { + + @InjectMocks + private LlmTaskQueryService llmTaskQueryService; + + @Mock + private LlmTaskQueryRepository llmTaskQueryRepository; + + @Mock + private DocumentRepository documentRepository; + + @Mock + private SummaryRepository summaryRepository; + + @Mock + private ProblemRepository problemRepository; + + @Test + void 작업_상태_확인시_존재하지_않는_문서ID로_요청한_경우_예외_발생() { + // given + given(documentRepository.existsById(anyLong())).willReturn(false); + + // when & then + assertAll(() -> assertThrows(NotFoundException.class, () -> llmTaskQueryService.fetchOverallStatus(1L)), + () -> verify(documentRepository).existsById(anyLong()) + ); + } + + @Test + void 작업_상태_확인시_모든_페이지의_작업이_완료된_경우_COMPLETED() { + // given + Long documentId = 1L; + List summaryIds = List.of(1L, 2L, 3L); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryIdsByDocumentId(documentId)).willReturn(summaryIds); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(1L)).willReturn(COMPLETED); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(2L)).willReturn(COMPLETED); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(3L)).willReturn(COMPLETED); + + // when + LlmTaskOverallStatusResult result = llmTaskQueryService.fetchOverallStatus(documentId); + + // then + assertAll(() -> assertThat(result.overallStatus()).isEqualTo(COMPLETED), + () -> assertThat(result.totalPages()).isEqualTo(3), + () -> assertThat(result.completedPages()).isEqualTo(3), + () -> verify(documentRepository).existsById(documentId), + () -> verify(summaryRepository).getSummaryIdsByDocumentId(documentId), + () -> verify(llmTaskQueryRepository).getTaskStatusBySummaryId(documentId) + ); + } + + @Test + void 작업_상태_확인시_모든_페이지의_작업이_완료되지_않은_경우_IN_PROGRESS() { + // given + Long documentId = 1L; + List summaryIds = List.of(1L, 2L, 3L); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryIdsByDocumentId(documentId)).willReturn(summaryIds); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(1L)).willReturn(COMPLETED); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(2L)).willReturn(IN_PROGRESS); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(3L)).willReturn(PENDING); + + // when + LlmTaskOverallStatusResult result = llmTaskQueryService.fetchOverallStatus(documentId); + + // then + assertAll(() -> assertThat(result.overallStatus()).isEqualTo(IN_PROGRESS), + () -> assertThat(result.totalPages()).isEqualTo(3), + () -> assertThat(result.completedPages()).isEqualTo(1), + () -> verify(documentRepository).existsById(documentId), + () -> verify(summaryRepository).getSummaryIdsByDocumentId(documentId), + () -> verify(llmTaskQueryRepository).getTaskStatusBySummaryId(documentId) + ); + } + + @Test + void 페이지별_작업_상태_확인() { + // given + Long documentId = 1L; + Long summaryId = 1L; + Integer pageNumber = 20; + LlmTaskPageStatusCommand command = new LlmTaskPageStatusCommand(documentId, pageNumber); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryIdByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(summaryId); + given(llmTaskQueryRepository.getTaskStatusBySummaryId(summaryId)).willReturn(IN_PROGRESS); + + // when + LlmTaskPageStatusResult result = llmTaskQueryService.fetchPageStatus(command); + + // then + assertThat(result.status()).isEqualTo(IN_PROGRESS); + } + + @Test + void 페이지별_작업_상태_확인시_요청_기록이_없는_경우_NOT_REQUESTED() { + // given + Long documentId = 1L; + Integer pageNumber = 20; + LlmTaskPageStatusCommand command = new LlmTaskPageStatusCommand(documentId, pageNumber); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryIdByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(null); + + // when + LlmTaskPageStatusResult result = llmTaskQueryService.fetchPageStatus(command); + + // then + assertThat(result.status()).isEqualTo(NOT_REQUESTED); + } + + @Test + void 작업_결과_확인시_존재하지_않는_문서ID로_요청한_경우_예외_발생() { + // given + given(documentRepository.existsById(anyLong())).willReturn(false); + + // when & then + assertAll(() -> assertThrows(NotFoundException.class, () -> llmTaskQueryService.findAllPagesResult(1L)), + () -> verify(documentRepository).existsById(anyLong()) + ); + } + + @Test + void 작업_결과_확인시_생성된_요약과_문제의_수가_일치하지_않는_경우_예외_발생() { + // given + Long documentId = 1L; + List summaryResults = List.of(new SummaryPageContentResult(1, "요약 내용")); + List problemResults = List.of(new ProblemPageContentResult(1, "문제 내용"), + new ProblemPageContentResult(2, "문제 내용") + ); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(summaryResults); + given(problemRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(problemResults); + + // when & then + assertAll(() -> assertThrows(InternalServerErrorException.class, () -> llmTaskQueryService.findAllPagesResult(1L)), + () -> verify(documentRepository).existsById(documentId), + () -> verify(summaryRepository).getPageNumbersAndContentByDocumentId(documentId), + () -> verify(problemRepository).getPageNumbersAndContentByDocumentId(documentId) + ); + } + + @Test + void 작업_결과_확인() { + // given + Long documentId = 1L; + List summaryResults = List.of(new SummaryPageContentResult(1, "요약 내용"), + new SummaryPageContentResult(2, "요약 내용") + ); + List problemResults = List.of(new ProblemPageContentResult(1, "문제 내용"), + new ProblemPageContentResult(2, "문제 내용") + ); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(summaryResults); + given(problemRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(problemResults); + + // when + LlmTaskAllPagesResult response = llmTaskQueryService.findAllPagesResult(documentId); + + // then + assertAll(() -> assertEquals(documentId, response.documentId()), + () -> assertEquals(2, response.results().size()), + () -> verify(documentRepository).existsById(documentId), + () -> verify(summaryRepository).getPageNumbersAndContentByDocumentId(documentId), + () -> verify(problemRepository).getPageNumbersAndContentByDocumentId(documentId) + ); + } + + @Test + void 페이지별_작업_결과_확인() { + // given + Long documentId = 1L; + Integer pageNumber = 20; + String summaryResult = "요약 내용"; + String problemResult = "문제 내용"; + LlmTaskPageResultCommand command = new LlmTaskPageResultCommand(documentId, pageNumber); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryContentByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(summaryResult); + given(problemRepository.getProblemContentByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(problemResult); + + // when + LlmTaskPageResult result = llmTaskQueryService.findPageResult(command); + + // then + assertAll( + () -> assertThat(result.summary()).isEqualTo(summaryResult), + () -> assertThat(result.problem()).isEqualTo(problemResult) + ); + } + + @Test + void 페이지별_작업_결과가_존재하지_않는_경우_예외가_아니라_null_응답() { + // given + Long documentId = 1L; + Integer pageNumber = 20; + LlmTaskPageResultCommand command = new LlmTaskPageResultCommand(documentId, pageNumber); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryContentByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(null); + given(problemRepository.getProblemContentByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(null); + + // when + LlmTaskPageResult result = llmTaskQueryService.findPageResult(command); + + // then + assertAll( + () -> assertThat(result.summary()).isEqualTo(null), + () -> assertThat(result.problem()).isEqualTo(null) + ); + } + + @Test + void 페이지별_작업_결과_확인시_요약과_문제_중_하나만_null인_경우_예외_발생() { + // given + Long documentId = 1L; + Integer pageNumber = 20; + String summaryResult = "요약 내용"; + LlmTaskPageResultCommand command = new LlmTaskPageResultCommand(documentId, pageNumber); + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryContentByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(summaryResult); + given(problemRepository.getProblemContentByDocumentIdAndPageNumber(documentId, pageNumber)).willReturn(null); + + // when & then + assertThrows(InternalServerErrorException.class, () -> llmTaskQueryService.findPageResult(command)); + } + + @Test + void 요청_기록이_없는_문서의_작업_상태_확인시_NOT_REQUESTED() { + // given + Long documentId = 1L; + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getSummaryIdsByDocumentId(documentId)).willReturn(Collections.emptyList()); + + // when + LlmTaskOverallStatusResult result = llmTaskQueryService.fetchOverallStatus(documentId); + + // then + assertAll( + () -> assertThat(result.overallStatus()).isEqualTo(NOT_REQUESTED), + () -> assertThat(result.totalPages()).isEqualTo(0), + () -> assertThat(result.completedPages()).isEqualTo(0) + ); + } + + @Test + void 요청_기록이_없는_문서의_결과_확인시_empty_list_응답() { + // given + Long documentId = 1L; + + given(documentRepository.existsById(anyLong())).willReturn(true); + given(summaryRepository.getPageNumbersAndContentByDocumentId(documentId)).willReturn(Collections.emptyList()); + + // when + LlmTaskAllPagesResult result = llmTaskQueryService.findAllPagesResult(documentId); + + // then + assertAll( + () -> assertThat(result.totalPages()).isEqualTo(0), + () -> assertThat(result.results()).isEmpty() + ); + } +} diff --git a/src/test/java/notai/llm/application/LLMServiceTest.java b/src/test/java/notai/llm/application/LlmTaskServiceTest.java similarity index 80% rename from src/test/java/notai/llm/application/LLMServiceTest.java rename to src/test/java/notai/llm/application/LlmTaskServiceTest.java index 2523c47..1f2f1b7 100644 --- a/src/test/java/notai/llm/application/LLMServiceTest.java +++ b/src/test/java/notai/llm/application/LlmTaskServiceTest.java @@ -9,11 +9,11 @@ import notai.document.domain.Document; import notai.document.domain.DocumentRepository; import notai.folder.domain.Folder; -import notai.llm.application.command.LLMSubmitCommand; +import notai.llm.application.command.LlmTaskSubmitCommand; import notai.llm.application.command.SummaryAndProblemUpdateCommand; -import notai.llm.application.result.LLMSubmitResult; -import notai.llm.domain.LLM; -import notai.llm.domain.LLMRepository; +import notai.llm.application.result.LlmTaskSubmitResult; +import notai.llm.domain.LlmTask; +import notai.llm.domain.LlmTaskRepository; import notai.member.domain.Member; import notai.member.domain.OauthId; import notai.member.domain.OauthProvider; @@ -36,13 +36,13 @@ import java.util.UUID; @ExtendWith(MockitoExtension.class) -class LLMServiceTest { +class LlmTaskServiceTest { @InjectMocks - private LLMService llmService; + private LlmTaskService llmTaskService; @Mock - private LLMRepository llmRepository; + private LlmTaskRepository llmTaskRepository; @Mock private DocumentRepository documentRepository; @@ -64,14 +64,14 @@ class LLMServiceTest { // given Long documentId = 1L; List pages = List.of(1, 2, 3); - LLMSubmitCommand command = new LLMSubmitCommand(documentId, pages); + LlmTaskSubmitCommand command = new LlmTaskSubmitCommand(documentId, pages); given(documentRepository.getById(anyLong())).willThrow(NotFoundException.class); // when & then - assertAll(() -> assertThrows(NotFoundException.class, () -> llmService.submitTask(command)), + assertAll(() -> assertThrows(NotFoundException.class, () -> llmTaskService.submitTasks(command)), () -> verify(documentRepository, times(1)).getById(documentId), - () -> verify(llmRepository, never()).save(any(LLM.class)) + () -> verify(llmTaskRepository, never()).save(any(LlmTask.class)) ); } @@ -80,11 +80,11 @@ class LLMServiceTest { // given Long documentId = 1L; List pages = List.of(1, 2); - LLMSubmitCommand command = new LLMSubmitCommand(documentId, pages); + LlmTaskSubmitCommand command = new LlmTaskSubmitCommand(documentId, pages); Member member = new Member(new OauthId("12345", OauthProvider.KAKAO), "test@example.com", "TestUser"); Folder folder = new Folder(member, "TestFolder"); - Document document = new Document(folder, "TestDocument", "http://example.com/test.pdf"); + Document document = new Document(folder, "TestDocument", "http://example.com/test.pdf", 43); List annotations = List.of(new Annotation(document, 1, 10, 20, 100, 50, "Annotation 1"), new Annotation(document, 1, 30, 40, 80, 60, "Annotation 2"), @@ -97,16 +97,16 @@ class LLMServiceTest { given(documentRepository.getById(anyLong())).willReturn(document); given(annotationRepository.findByDocumentId(anyLong())).willReturn(annotations); given(aiClient.submitLlmTask(any(LlmTaskRequest.class))).willReturn(taskResponse); - given(llmRepository.save(any(LLM.class))).willAnswer(invocation -> invocation.getArgument(0)); + given(llmTaskRepository.save(any(LlmTask.class))).willAnswer(invocation -> invocation.getArgument(0)); // when - LLMSubmitResult result = llmService.submitTask(command); + LlmTaskSubmitResult result = llmTaskService.submitTasks(command); // then assertAll(() -> verify(documentRepository, times(1)).getById(documentId), () -> verify(annotationRepository, times(1)).findByDocumentId(documentId), () -> verify(aiClient, times(2)).submitLlmTask(any(LlmTaskRequest.class)), - () -> verify(llmRepository, times(2)).save(any(LLM.class)) + () -> verify(llmTaskRepository, times(2)).save(any(LlmTask.class)) ); verify(aiClient).submitLlmTask(argThat(request -> request.keyboardNote().equals("Annotation 1, Annotation 2"))); @@ -123,17 +123,16 @@ class LLMServiceTest { String problemContent = "문제 내용"; Integer pageNumber = 5; - LLM taskRecord = mock(LLM.class); + LlmTask taskRecord = mock(LlmTask.class); Summary summary = mock(Summary.class); Problem problem = mock(Problem.class); SummaryAndProblemUpdateCommand command = new SummaryAndProblemUpdateCommand(taskId, - pageNumber, summaryContent, problemContent ); - given(llmRepository.getById(any(UUID.class))).willReturn(taskRecord); + given(llmTaskRepository.getById(any(UUID.class))).willReturn(taskRecord); given(summaryRepository.getById(anyLong())).willReturn(summary); given(problemRepository.getById(anyLong())).willReturn(problem); @@ -141,15 +140,16 @@ class LLMServiceTest { given(taskRecord.getProblem()).willReturn(problem); given(summary.getId()).willReturn(summaryId); given(problem.getId()).willReturn(problemId); + given(summary.getPageNumber()).willReturn(pageNumber); // when - Integer resultPageNumber = llmService.updateSummaryAndProblem(command); + Integer resultPageNumber = llmTaskService.updateSummaryAndProblem(command); // then assertAll(() -> verify(taskRecord).completeTask(), () -> verify(summary).updateContent(summaryContent), () -> verify(problem).updateContent(problemContent), - () -> verify(llmRepository, times(1)).save(taskRecord), + () -> verify(llmTaskRepository, times(1)).save(taskRecord), () -> verify(summaryRepository, times(1)).save(summary), () -> verify(problemRepository, times(1)).save(problem), () -> assertEquals(pageNumber, resultPageNumber) diff --git a/src/test/java/notai/ocr/application/OCRServiceTest.java b/src/test/java/notai/ocr/application/OCRServiceTest.java index 50c0b38..8916fe5 100644 --- a/src/test/java/notai/ocr/application/OCRServiceTest.java +++ b/src/test/java/notai/ocr/application/OCRServiceTest.java @@ -4,6 +4,7 @@ import notai.ocr.domain.OCR; import notai.ocr.domain.OCRRepository; import notai.pdf.result.PdfSaveResult; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.mockito.ArgumentMatchers.any; @@ -14,9 +15,8 @@ import org.springframework.core.io.ClassPathResource; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +@Tag("exclude-test") @ExtendWith(MockitoExtension.class) class OCRServiceTest { @@ -24,26 +24,18 @@ class OCRServiceTest { OCRService ocrService; @Mock OCRRepository ocrRepository; - + @Test - void savePdf_success_existsTestPdf() throws IOException { + void saveOCR_success_existsTestPdf() throws IOException { //given Document document = mock(Document.class); OCR ocr = mock(OCR.class); ClassPathResource existsPdf = new ClassPathResource("pdf/test.pdf"); - PdfSaveResult saveResult = PdfSaveResult.of("test.pdf", existsPdf.getFile()); + PdfSaveResult saveResult = PdfSaveResult.of("test.pdf", existsPdf.getFile(), 43); when(ocrRepository.save(any(OCR.class))).thenReturn(ocr); //when ocrService.saveOCR(document, saveResult.pdf()); //then verify(ocrRepository, times(43)).save(any(OCR.class)); - - deleteFile(saveResult.pdf().toPath()); - } - - void deleteFile(Path filePath) throws IOException { - if (Files.exists(filePath)) { - Files.delete(filePath); - } } }