diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7262a56 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,48 @@ +name: Deploy to Production + +on: + push: + branches: + - 'release/**' + workflow_dispatch: + +jobs: + deploy: + name: Production Deploy + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + with: + submodules: true + token: ${{ secrets.ACTION_TOKEN }} + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'corretto' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build with Gradle + run: | + pwd + ls -la + chmod +x ./gradlew + ./gradlew clean bootJar + + - name: Copy files and Deploy + run: | + cp -r ./build/libs/*.jar /home/yugyeom/notai/ + cd /home/yugyeom/notai + docker-compose down + docker-compose up -d --build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fc0f695 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "Team29_BE_Submodule"] + path = Team29_BE_Submodule + url = git@github.com:29ana-notai/Team29_BE_Submodule.git + branch = release/0.0.2 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 6442c2e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM gradle:jdk21-jammy - -WORKDIR /app - -CMD ["gradle", "bootRun"] \ No newline at end of file diff --git a/Team29_BE_Submodule b/Team29_BE_Submodule new file mode 160000 index 0000000..9786d71 --- /dev/null +++ b/Team29_BE_Submodule @@ -0,0 +1 @@ +Subproject commit 9786d71108d3333313244c73fb01e56b6993916f diff --git a/build.gradle b/build.gradle index 79dab35..60edd53 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'notai' -version = '0.0.1-SNAPSHOT' +version = '0.0.2' java { toolchain { @@ -59,6 +59,9 @@ dependencies { // OCR implementation 'net.sourceforge.tess4j:tess4j:5.13.0' + + // Slack + implementation("com.slack.api:slack-api-client:1.44.1") } tasks.named('test') { @@ -70,3 +73,11 @@ test { excludeTags 'exclude-test' } } + +processResources.dependsOn('copySecret') + +tasks.register('copySecret', Copy) { + from './Team29_BE_Submodule' + include "**" + into './src/main/resources' +} diff --git a/settings.gradle b/settings.gradle index 0f5036d..21eb53a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'backend' +rootProject.name = 'notai' diff --git a/src/main/java/notai/client/slack/SlackWebHookClient.java b/src/main/java/notai/client/slack/SlackWebHookClient.java new file mode 100644 index 0000000..58f8947 --- /dev/null +++ b/src/main/java/notai/client/slack/SlackWebHookClient.java @@ -0,0 +1,44 @@ +package notai.client.slack; + +import com.slack.api.Slack; +import com.slack.api.webhook.Payload; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import notai.common.exception.ErrorMessages; +import notai.common.exception.type.ExternalApiException; +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Async +@Slf4j +public class SlackWebHookClient { + + private final SlackWebHookProperty slackWebHookProperty; + + public void sendToInfoChannel(String message) { + Slack slack = Slack.getInstance(); + Payload payload = Payload.builder().text(message).build(); + + try { + slack.send(slackWebHookProperty.infoChannel().webhookUrl(), payload); + } catch (IOException e) { + throw new ExternalApiException(ErrorMessages.SLACK_API_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + } + + public void sendToErrorChannel(String message) { + Slack slack = Slack.getInstance(); + Payload payload = Payload.builder().text(message).build(); + + try { + slack.send(slackWebHookProperty.errorChannel().webhookUrl(), payload); + } catch (IOException e) { + throw new ExternalApiException(ErrorMessages.SLACK_API_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + } +} diff --git a/src/main/java/notai/client/slack/SlackWebHookProperty.java b/src/main/java/notai/client/slack/SlackWebHookProperty.java new file mode 100644 index 0000000..01a64f1 --- /dev/null +++ b/src/main/java/notai/client/slack/SlackWebHookProperty.java @@ -0,0 +1,14 @@ +package notai.client.slack; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "slack") +public record SlackWebHookProperty( + WebhookProperty infoChannel, + WebhookProperty errorChannel +) { + public record WebhookProperty( + String webhookUrl + ) { + } +} diff --git a/src/main/java/notai/common/config/SwaggerConfig.java b/src/main/java/notai/common/config/SwaggerConfig.java index f3f77db..bcf6279 100644 --- a/src/main/java/notai/common/config/SwaggerConfig.java +++ b/src/main/java/notai/common/config/SwaggerConfig.java @@ -38,6 +38,6 @@ public OpenAPI openAPI() { } private Info apiInfo() { - return new Info().title("notai API").description("notai API 문서입니다.").version("0.0.1"); + return new Info().title("notai API").description("notai API 문서입니다.").version("0.0.2"); } } diff --git a/src/main/java/notai/common/exception/ApplicationException.java b/src/main/java/notai/common/exception/ApplicationException.java index c6bc1b3..10552ca 100644 --- a/src/main/java/notai/common/exception/ApplicationException.java +++ b/src/main/java/notai/common/exception/ApplicationException.java @@ -11,4 +11,9 @@ public ApplicationException(ErrorMessages message, int code) { super(message.getMessage()); this.code = code; } + + public ApplicationException(String message, int code) { + super(message); + this.code = code; + } } diff --git a/src/main/java/notai/common/exception/ErrorMessages.java b/src/main/java/notai/common/exception/ErrorMessages.java index bc7a178..ecbce32 100644 --- a/src/main/java/notai/common/exception/ErrorMessages.java +++ b/src/main/java/notai/common/exception/ErrorMessages.java @@ -38,6 +38,7 @@ public enum ErrorMessages { // external api call KAKAO_API_ERROR("카카오 API 호출에 예외가 발생했습니다."), AI_SERVER_ERROR("AI 서버 API 호출에 예외가 발생했습니다."), + SLACK_API_ERROR("슬랙 API 호출에 예외가 발생했습니다."), // auth INVALID_ACCESS_TOKEN("유효하지 않은 토큰입니다."), diff --git a/src/main/java/notai/common/exception/ExceptionControllerAdvice.java b/src/main/java/notai/common/exception/ExceptionControllerAdvice.java index c01f2d4..e62be1b 100644 --- a/src/main/java/notai/common/exception/ExceptionControllerAdvice.java +++ b/src/main/java/notai/common/exception/ExceptionControllerAdvice.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import notai.client.slack.SlackWebHookClient; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; @@ -25,11 +26,19 @@ @RestControllerAdvice public class ExceptionControllerAdvice extends ResponseEntityExceptionHandler { + private final SlackWebHookClient slackWebHookClient; + @ExceptionHandler(ApplicationException.class) ResponseEntity handleException(HttpServletRequest request, ApplicationException e) { log.info("잘못된 요청이 들어왔습니다. uri: {} {}, 내용: {}", request.getMethod(), request.getRequestURI(), e.getMessage()); + slackWebHookClient.sendToInfoChannel( + "잘못된 요청이 들어왔습니다.\n" + + "uri: " + request.getMethod() + " " + request.getRequestURI() + "\n" + + "내용: " + e.getMessage() + ); + requestLogging(request); return ResponseEntity.status(e.getCode()).body(new ExceptionResponse(e.getMessage())); @@ -39,6 +48,12 @@ ResponseEntity handleException(HttpServletRequest request, Ap ResponseEntity handleException(HttpServletRequest request, Exception e) { log.error("예상하지 못한 예외가 발생했습니다. uri: {} {}, ", request.getMethod(), request.getRequestURI(), e); + slackWebHookClient.sendToErrorChannel( + "예상하지 못한 예외가 발생했습니다.\n" + + "uri: " + request.getMethod() + " " + request.getRequestURI() + "\n" + + "내용: " + e.getMessage() + ); + requestLogging(request); return ResponseEntity.internalServerError().body(new ExceptionResponse(e.getMessage())); } @@ -57,6 +72,12 @@ protected ResponseEntity handleExceptionInternal( HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); log.error("예외가 발생했습니다. uri: {} {}, ", request.getMethod(), request.getRequestURI(), e); + slackWebHookClient.sendToErrorChannel( + "예외가 발생했습니다.\n" + + "uri: " + request.getMethod() + " " + request.getRequestURI() + "\n" + + " 내용: " + e.getMessage() + ); + requestLogging(request); return ResponseEntity.status(statusCode).body(new ExceptionResponse(e.getMessage())); } diff --git a/src/main/java/notai/ocr/application/OCRService.java b/src/main/java/notai/ocr/application/OCRService.java index 55353ba..f5c9a21 100644 --- a/src/main/java/notai/ocr/application/OCRService.java +++ b/src/main/java/notai/ocr/application/OCRService.java @@ -10,6 +10,7 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.PDFRenderer; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -22,27 +23,32 @@ public class OCRService { private final OCRRepository ocrRepository; + @Value("${tesseract.library.path}") + private String libraryPath; + + @Value("${tesseract.data.path}") + private String dataPath; + + @Value("${tesseract.language}") + private String language; + @Async public void saveOCR( Document document, File pdfFile ) { try { - // System.setProperty("jna.library.path", "/usr/local/opt/tesseract/lib/"); - System.setProperty("jna.library.path", "C:\\Program Files\\Tesseract-OCR"); + System.setProperty("jna.library.path", libraryPath); - //window, mac -> brew install tesseract, tesseract-lang Tesseract tesseract = new Tesseract(); - - // tesseract.setDatapath("/usr/local/share/tessdata"); - tesseract.setDatapath("C:\\Program Files\\Tesseract-OCR\\tessdata"); - tesseract.setLanguage("kor+eng"); + tesseract.setDatapath(dataPath); + tesseract.setLanguage(language); PDDocument pdDocument = Loader.loadPDF(pdfFile); PDFRenderer pdfRenderer = new PDFRenderer(pdDocument); for (int i = 0; i < pdDocument.getNumberOfPages(); i++) { BufferedImage image = pdfRenderer.renderImage(i); String ocrResult = tesseract.doOCR(image); - OCR ocr = new OCR(document, i + 1, ocrResult); + OCR ocr = new OCR(document, i, ocrResult); ocrRepository.save(ocr); } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index da70f6b..621ee0f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -29,6 +29,11 @@ spring: mvc: converters: preferred-json-mapper: gson + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + enabled: true server: servlet: @@ -43,3 +48,16 @@ token: # todo production에서 secretKey 변경 secretKey: "ZGQrT0tuZHZkRWRxeXJCamRYMDFKMnBaR2w5WXlyQm9HU2RqZHNha1gycFlkMWpLc0dObw==" accessTokenExpirationMillis: 10000000000 refreshTokenExpirationMillis: 10000000000 + +file: + audio: + basePath: /app/audio/ + pdf: + basePath: /app/pdf/ + +tesseract: + library: + path: C:\\Program Files\\Tesseract-OCR + data: + path: C:\\Program Files\\Tesseract-OCR\\tessdata + language: kor+eng diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3baccb8..fadf7d2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,4 @@ spring: profiles: - active: local + active: prod -file: - audio: - basePath: /app/audio/ # Docker 볼륨에 맞게 경로 수정 \ No newline at end of file