Skip to content

Commit

Permalink
Merge pull request kakao-tech-campus-2nd-step3#59 from kakao-tech-cam…
Browse files Browse the repository at this point in the history
…pus-2nd-step3/Develop

Feat: AI Client, PDF 처리 및 OCR 기능 구현
  • Loading branch information
hynseoj authored Oct 11, 2024
2 parents 780b049 + 50bbe10 commit f92c827
Show file tree
Hide file tree
Showing 56 changed files with 730 additions and 85 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
}

test {
useJUnitPlatform {
excludeTags 'exclude-test'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import java.util.List;
import java.util.stream.Collectors;

import static notai.common.exception.ErrorMessages.ANNOTATION_NOT_FOUND;

@Service
@RequiredArgsConstructor
public class AnnotationQueryService {
Expand All @@ -25,7 +27,7 @@ public List<AnnotationResponse> getAnnotationsByDocumentAndPageNumbers(Long docu

List<Annotation> annotations = annotationRepository.findByDocumentIdAndPageNumberIn(documentId, pageNumbers);
if (annotations.isEmpty()) {
throw new NotFoundException("해당 문서에 해당 페이지 번호의 주석이 존재하지 않습니다.");
throw new NotFoundException(ANNOTATION_NOT_FOUND);
}

return annotations.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.util.List;
import java.util.Optional;

import static notai.common.exception.ErrorMessages.ANNOTATION_NOT_FOUND;

public interface AnnotationRepository extends JpaRepository<Annotation, Long> {

List<Annotation> findByDocumentIdAndPageNumberIn(Long documentId, List<Integer> pageNumbers);
Expand All @@ -15,6 +17,10 @@ public interface AnnotationRepository extends JpaRepository<Annotation, Long> {

default Annotation getById(Long annotationId) {
return findById(annotationId)
.orElseThrow(() -> new NotFoundException("주석을 찾을 수 없습니다. ID: " + annotationId));
.orElseThrow(() -> new NotFoundException(ANNOTATION_NOT_FOUND));
}

List<Annotation> findByDocumentIdAndPageNumber(Long documentId, Integer pageNumber);

List<Annotation> findByDocumentId(Long documentId);
}
5 changes: 3 additions & 2 deletions src/main/java/notai/auth/Auth.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package notai.auth;

import io.swagger.v3.oas.annotations.Hidden;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Hidden
@Target(PARAMETER)
@Retention(RUNTIME)
Expand Down
8 changes: 5 additions & 3 deletions src/main/java/notai/auth/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import javax.crypto.SecretKey;
import java.util.Date;

import static notai.common.exception.ErrorMessages.*;

@Component
public class TokenService {
private static final String MEMBER_ID_CLAIM = "memberId";
Expand Down Expand Up @@ -57,9 +59,9 @@ public TokenPair refreshTokenPair(String refreshToken) {
try {
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(refreshToken);
} catch (ExpiredJwtException e) {
throw new UnAuthorizedException("만료된 Refresh Token입니다.");
throw new UnAuthorizedException(EXPIRED_REFRESH_TOKEN);
} catch (Exception e) {
throw new UnAuthorizedException("유효하지 않은 Refresh Token입니다.");
throw new UnAuthorizedException(INVALID_REFRESH_TOKEN);
}
Member member = memberRepository.getByRefreshToken(refreshToken);

Expand All @@ -72,7 +74,7 @@ public Long extractMemberId(String token) {
Long.class
);
} catch (Exception e) {
throw new UnAuthorizedException("유효하지 않은 토큰입니다.");
throw new UnAuthorizedException(INVALID_ACCESS_TOKEN);
}
}
}
18 changes: 18 additions & 0 deletions src/main/java/notai/client/ai/AiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package notai.client.ai;

import notai.client.ai.request.LlmTaskRequest;
import notai.client.ai.response.TaskResponse;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.service.annotation.PostExchange;

public interface AiClient {

@PostExchange(url = "/api/ai/llm")
TaskResponse submitLlmTask(@RequestBody LlmTaskRequest request);

@PostExchange(url = "/api/ai/stt")
TaskResponse submitSttTask(@RequestPart("audio") MultipartFile audioFile);
}

34 changes: 34 additions & 0 deletions src/main/java/notai/client/ai/AiClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package notai.client.ai;

import lombok.extern.slf4j.Slf4j;
import notai.common.exception.type.ExternalApiException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.client.RestClient;

import static notai.client.HttpInterfaceUtil.createHttpInterface;
import static notai.common.exception.ErrorMessages.AI_SERVER_ERROR;

@Slf4j
@Configuration
public class AiClientConfig {

@Value("${ai-server-url}")
private String aiServerUrl;

@Bean
public AiClient aiClient() {
RestClient restClient =
RestClient.builder().baseUrl(aiServerUrl).requestInterceptor((request, body, execution) -> {
request.getHeaders().setContentLength(body.length); // Content-Length 설정 안하면 411 에러 발생
return execution.execute(request, body);
}).defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
String responseBody = new String(response.getBody().readAllBytes());
log.error("Response Status: {}", response.getStatusCode());
throw new ExternalApiException(AI_SERVER_ERROR, response.getStatusCode().value());
}).build();
return createHttpInterface(restClient, AiClient.class);
}
}
11 changes: 11 additions & 0 deletions src/main/java/notai/client/ai/request/LlmTaskRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package notai.client.ai.request;

public record LlmTaskRequest(
String ocrText,
String stt,
String keyboardNote
) {
public static LlmTaskRequest of(String ocrText, String stt, String keyboardNote) {
return new LlmTaskRequest(ocrText, stt, keyboardNote);
}
}
8 changes: 8 additions & 0 deletions src/main/java/notai/client/ai/request/SttTaskRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package notai.client.ai.request;

import org.springframework.web.multipart.MultipartFile;

public record SttTaskRequest(
MultipartFile audioFile
) {
}
9 changes: 9 additions & 0 deletions src/main/java/notai/client/ai/response/TaskResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package notai.client.ai.response;

import java.util.UUID;

public record TaskResponse(
UUID taskId,
String taskType
) {
}
3 changes: 2 additions & 1 deletion src/main/java/notai/client/oauth/OauthClientComposite.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static notai.common.exception.ErrorMessages.INVALID_LOGIN_TYPE;

@Component
public class OauthClientComposite {
Expand All @@ -27,6 +28,6 @@ public Member fetchMember(OauthProvider oauthProvider, String accessToken) {

public OauthClient getOauthClient(OauthProvider oauthProvider) {
return Optional.ofNullable(oauthClients.get(oauthProvider))
.orElseThrow(() -> new BadRequestException("지원하지 않는 소셜 로그인 타입입니다."));
.orElseThrow(() -> new BadRequestException(INVALID_LOGIN_TYPE));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.web.client.RestClient;

import static notai.client.HttpInterfaceUtil.createHttpInterface;
import static notai.common.exception.ErrorMessages.KAKAO_API_ERROR;

@Slf4j
@Configuration
Expand All @@ -19,7 +20,7 @@ public KakaoClient kakaoClient() {
(request, response) -> {
String responseData = new String(response.getBody().readAllBytes());
log.error("카카오톡 API 오류 : {}", responseData);
throw new ExternalApiException(responseData, response.getStatusCode().value());
throw new ExternalApiException(KAKAO_API_ERROR, response.getStatusCode().value());
}
).build();
return createHttpInterface(restClient, KakaoClient.class);
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/notai/common/config/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ public class AuthConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/api/**").excludePathPatterns(
"/api/members/oauth/login/**").excludePathPatterns("/api/members/token/refresh");
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/members/oauth/login/**")
.excludePathPatterns("/api/members/token/refresh");
}

@Override
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/notai/common/config/AuthInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import notai.auth.TokenService;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;

@Component
public class AuthInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/notai/common/converter/JsonAttributeConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import lombok.extern.slf4j.Slf4j;
import notai.common.exception.type.JsonConversionException;

import java.io.IOException;

import static notai.common.exception.ErrorMessages.JSON_CONVERSION_ERROR;

@Slf4j
@Converter
public class JsonAttributeConverter<T> implements AttributeConverter<T, String> {

Expand All @@ -24,7 +28,8 @@ public String convertToDatabaseColumn(T attribute) {
try {
return objectMapper.writeValueAsString(attribute);
} catch (JsonProcessingException e) {
throw new JsonConversionException("객체를 JSON 문자열로 변환하는 중 오류가 발생했습니다.");
log.error("객체를 JSON 문자열로 변환하는 중 오류가 발생했습니다.");
throw new JsonConversionException(JSON_CONVERSION_ERROR);
}
}

Expand All @@ -33,7 +38,8 @@ public T convertToEntityAttribute(String dbData) {
try {
return objectMapper.readValue(dbData, typeReference);
} catch (IOException e) {
throw new JsonConversionException("JSON 문자열을 객체로 변환하는 중 오류가 발생했습니다.");
log.error("JSON 문자열을 객체로 변환하는 중 오류가 발생했습니다.");
throw new JsonConversionException(JSON_CONVERSION_ERROR);
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/notai/common/domain/RootEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import static lombok.AccessLevel.PROTECTED;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
Expand All @@ -13,6 +12,8 @@
import java.time.LocalDateTime;
import java.util.Objects;

import static lombok.AccessLevel.PROTECTED;

@Getter
@NoArgsConstructor(access = PROTECTED)
@EntityListeners(AuditingEntityListener.class)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/notai/common/domain/vo/FilePath.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import notai.common.exception.type.BadRequestException;

import static lombok.AccessLevel.PROTECTED;
import static notai.common.exception.ErrorMessages.INVALID_FILE_TYPE;

@Embeddable
@Getter
Expand All @@ -24,7 +25,7 @@ public static FilePath from(String filePath) {
// 추후 확장자 추가
if (!filePath.matches(
"[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣()\\[\\]+\\-&/_\\s]+(/[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣()\\[\\]+\\-&/_\\s]+)*/?[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣()\\[\\]+\\-&/_\\s]+\\.mp3")) {
throw new BadRequestException("지원하지 않는 파일 형식입니다.");
throw new BadRequestException(INVALID_FILE_TYPE);
}
return new FilePath(filePath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ public class ApplicationException extends RuntimeException {

private final int code;

public ApplicationException(String message, int code) {
super(message);
public ApplicationException(ErrorMessages message, int code) {
super(message.getMessage());
this.code = code;
}
}
62 changes: 62 additions & 0 deletions src/main/java/notai/common/exception/ErrorMessages.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package notai.common.exception;

import lombok.Getter;

@Getter
public enum ErrorMessages {

// annotation
ANNOTATION_NOT_FOUND("주석을 찾을 수 없습니다."),

// document
DOCUMENT_NOT_FOUND("자료를 찾을 수 없습니다."),

// 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 요약 및 문제 생성 중에 문제가 발생했습니다."),

// problem
PROBLEM_NOT_FOUND("문제 정보를 찾을 수 없습니다."),

// summary
SUMMARY_NOT_FOUND("요약 정보를 찾을 수 없습니다."),

// member
MEMBER_NOT_FOUND("회원 정보를 찾을 수 없습니다."),

// recording
RECORDING_NOT_FOUND("녹음 파일을 찾을 수 없습니다."),

// external api call
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("지원하지 않는 소셜 로그인 타입입니다."),

// json conversion
JSON_CONVERSION_ERROR("JSON-객체 변환 중에 오류가 발생했습니다."),

// etc
INVALID_FILE_TYPE("지원하지 않는 파일 형식입니다."),
FILE_NOT_FOUND("존재하지 않는 파일입니다."),
FILE_SAVE_ERROR("파일을 저장하는 과정에서 오류가 발생했습니다."),
INVALID_AUDIO_ENCODING("오디오 파일이 잘못되었습니다.")
;

private final String message;

ErrorMessages(String message) {
this.message = message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@


import notai.common.exception.ApplicationException;
import notai.common.exception.ErrorMessages;

public class BadRequestException extends ApplicationException {

public BadRequestException(String message) {
public BadRequestException(ErrorMessages message) {
super(message, 400);
}
}
Loading

0 comments on commit f92c827

Please sign in to comment.