From 2b81c81c73b111d290f4062367565bcc3be392fa Mon Sep 17 00:00:00 2001 From: MoonSung Bae Date: Thu, 30 Apr 2026 03:13:23 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"Refactor:=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EA=B5=AC=ED=98=84=20"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +- build.gradle | 8 +- infra/.env.example | 13 - infra/docker-compose.stg.yml | 6 - perf/delete/README.md | 8 +- perf/{seed => delete}/seed_dataset.js | 10 +- perf/read/README.md | 8 +- perf/read/doc_list_benchmark.js | 6 +- perf/thumbnail/preview_e2e_benchmark.js | 194 ----------- .../thumbnail/thumbnail_workflow_benchmark.js | 300 ------------------ .../io/ejangs/docsa/DocsaApplication.java | 2 + .../domain/branch/app/BranchService.java | 12 +- .../app/create/BranchCreateOrchestrator.java | 10 +- .../branch/merge/app/MergeOrchestrator.java | 10 +- .../branch/merge/swagger/MergeDocs.java | 2 +- .../branch/swagger/CreateBranchDocs.java | 2 +- .../branch/swagger/DeleteBranchDocs.java | 28 +- .../branch/swagger/RenameBranchDocs.java | 26 +- .../domain/commit/app/CommitService.java | 12 +- .../app/create/CommitCreateOrchestrator.java | 10 +- .../app/create/CommitMongoTxService.java | 2 +- .../app/create/CommitMySqlTxService.java | 2 +- .../commit/swagger/CreateCommitDocs.java | 4 +- .../commit/swagger/DeleteCommitDocs.java | 8 +- .../docsa/domain/doc/app/DocService.java | 12 +- .../app/create/DocCreateMySqlTxService.java | 7 - .../doc/app/create/DocCreateOrchestrator.java | 10 +- .../doc/dto/response/DocPageResponse.java | 15 +- .../ejangs/docsa/domain/doc/entity/Doc.java | 5 - .../domain/doc/swagger/GetDocGraphDocs.java | 4 +- .../thumbnail/api/ThumbnailController.java | 44 --- .../thumbnail/app/ThumbnailQueryService.java | 29 -- .../doc/thumbnail/app/ThumbnailService.java | 99 ------ .../thumbnail/dao/ThumbnailRepository.java | 33 -- .../dto/ThumbnailFinalizeRequest.java | 22 -- .../doc/thumbnail/dto/ThumbnailResponse.java | 18 -- .../thumbnail/dto/ThumbnailSyncResponse.java | 16 - .../doc/thumbnail/entity/Thumbnail.java | 103 ------ .../swagger/FinalizeThumbnailDocs.java | 173 ---------- .../domain/doc/util/DocListAssembler.java | 98 +++--- .../docsa/domain/doc/util/DocMapper.java | 7 +- .../domain/image/app/ImageQueryService.java | 21 -- .../docsa/domain/image/app/ImageService.java | 18 +- .../dto/request/ImageUploadUrlRequest.java | 7 +- .../docsa/domain/image/entity/Image.java | 22 +- .../swagger/CreateImageUploadUrlDocs.java | 3 +- .../docsa/domain/save/app/SaveService.java | 13 +- .../save/dto/response/SaveUpdateResponse.java | 13 +- .../domain/save/swagger/UpdateSaveDocs.java | 7 +- .../docsa/domain/save/util/SaveMapper.java | 6 +- .../ejangs/docsa/global/config/JpaConfig.java | 6 +- .../exception/errorcode/BranchErrorCode.java | 13 +- .../exception/errorcode/CommitErrorCode.java | 3 +- .../errorcode/ThumbnailErrorCode.java | 19 -- .../app/MongoDeleteOutboxCreateService.java | 6 +- .../outbox}/app/MongoDeleteOutboxFactory.java | 14 +- .../MongoDeleteOutboxLifecycleService.java | 26 +- .../outbox}/app/MongoDeleteOutboxWorker.java | 11 +- .../outbox}/app/MongoDeleteService.java | 4 +- .../mysql/MongoDeleteOutboxRepository.java | 29 ++ .../outbox}/dto/CommitMongoIdsDto.java | 2 +- .../outbox}/dto/MongoIdsDto.java | 2 +- .../outbox}/entity/MongoDeleteOutbox.java | 69 +++- .../outbox}/util/MongoDeleteMapper.java | 4 +- .../outbox}/util/MongoIdsCollector.java | 4 +- .../docsa/global/outbox/BaseOutboxEntity.java | 71 ----- .../docsa/global/outbox/OutboxStatus.java | 8 - .../mysql/MongoDeleteOutboxRepository.java | 44 --- .../outbox/s3/app/S3DeleteOutboxFactory.java | 24 -- .../app/S3DeleteOutboxLifecycleService.java | 81 ----- .../outbox/s3/app/S3DeleteOutboxWorker.java | 71 ----- .../global/outbox/s3/app/S3DeleteService.java | 33 -- .../s3/dao/S3DeleteOutboxRepository.java | 45 --- .../global/outbox/s3/dto/S3DeleteTarget.java | 7 - .../outbox/s3/entity/S3DeleteOutbox.java | 48 --- src/main/resources/application-local.yml | 15 +- src/main/resources/application-prod.yml | 5 - src/main/resources/application-stg.yml | 16 +- .../db/migration/V1__init_schema.sql | 142 --------- ...V2__add_thumbnail_and_s3_delete_outbox.sql | 80 ----- .../ejangs/docsa/DocsaApplicationTests.java | 2 + .../domain/branch/app/BranchServiceTest.java | 10 +- ...ranchCreateConsistencyIntegrationTest.java | 10 +- .../create/BranchCreateOrchestratorTest.java | 10 +- .../app/CommitCreateOrchestratorTest.java | 10 +- .../commit/app/CommitServiceMockTest.java | 2 +- .../app/CreateCommitIntegrationTest.java | 7 +- .../app/DeleteCommitIntegrationTest.java | 4 +- .../DocServiceIntegrationTests.java | 14 +- .../app/ThumbnailServiceUnitTest.java | 139 -------- .../doc/unit/DocCreateOrchestratorTest.java | 8 +- .../domain/doc/unit/DocServiceUnitTests.java | 2 +- .../docsa/domain/doc/util/DocTestUtils.java | 6 +- .../image/app/ImageServiceUnitTest.java | 52 +-- .../domain/save/api/SaveControllerTest.java | 4 +- .../save/app/SaveServiceIntegrationTest.java | 9 +- .../domain/save/app/SaveServiceUnitTest.java | 15 +- .../outbox}/MongoTransactionTestService.java | 2 +- .../outbox}/MongoTransactionTests.java | 2 +- .../app/MongoDeleteOutboxFactoryUnitTest.java | 14 +- .../app/MongoDeleteOutboxIntegrationTest.java | 23 +- .../MongoDeleteOutboxRaceIntegrationTest.java | 16 +- .../s3/app/S3DeleteServiceUnitTest.java | 74 ----- src/test/resources/application-test.yml | 11 - 104 files changed, 411 insertions(+), 2442 deletions(-) rename perf/{seed => delete}/seed_dataset.js (97%) delete mode 100644 perf/thumbnail/preview_e2e_benchmark.js delete mode 100644 perf/thumbnail/thumbnail_workflow_benchmark.js delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/api/ThumbnailController.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailQueryService.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailService.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dao/ThumbnailRepository.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailFinalizeRequest.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailResponse.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailSyncResponse.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/entity/Thumbnail.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/doc/thumbnail/swagger/FinalizeThumbnailDocs.java delete mode 100644 src/main/java/io/ejangs/docsa/domain/image/app/ImageQueryService.java delete mode 100644 src/main/java/io/ejangs/docsa/global/exception/errorcode/ThumbnailErrorCode.java rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxCreateService.java (72%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxFactory.java (88%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxLifecycleService.java (71%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxWorker.java (86%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteService.java (90%) create mode 100644 src/main/java/io/ejangs/docsa/global/mongo/outbox/dao/mysql/MongoDeleteOutboxRepository.java rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/dto/CommitMongoIdsDto.java (70%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/dto/MongoIdsDto.java (94%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/entity/MongoDeleteOutbox.java (68%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/util/MongoDeleteMapper.java (86%) rename src/main/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/util/MongoIdsCollector.java (95%) delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/BaseOutboxEntity.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/OutboxStatus.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/mongo/dao/mysql/MongoDeleteOutboxRepository.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxFactory.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxLifecycleService.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxWorker.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteService.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/dao/S3DeleteOutboxRepository.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/dto/S3DeleteTarget.java delete mode 100644 src/main/java/io/ejangs/docsa/global/outbox/s3/entity/S3DeleteOutbox.java delete mode 100644 src/main/resources/db/migration/V1__init_schema.sql delete mode 100644 src/main/resources/db/migration/V2__add_thumbnail_and_s3_delete_outbox.sql delete mode 100644 src/test/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailServiceUnitTest.java rename src/test/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/MongoTransactionTestService.java (96%) rename src/test/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/MongoTransactionTests.java (97%) rename src/test/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxFactoryUnitTest.java (87%) rename src/test/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxIntegrationTest.java (91%) rename src/test/java/io/ejangs/docsa/global/{outbox/mongo => mongo/outbox}/app/MongoDeleteOutboxRaceIntegrationTest.java (90%) delete mode 100644 src/test/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteServiceUnitTest.java diff --git a/README.md b/README.md index f0bae2c1..833dfe36 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,20 @@ Docsa는 문서의 변경 이력과 다양한 버전을 효율적으로 관리 이 프로젝트의 목표는 Git을 모르는 사용자도 손쉽게 버전 관리를 활용하여 문서를 편집하고 운용할 수 있도록 돕는 것입니다.

사용자는 그래프로 구현된 시각적인 UI를 통해 문서를 직접 편집하고, 다양한 버전 흐름을 한눈에 확인하며 관리할 수 있습니다. -Docsa는 문서의 변경 사항을 기록(commit) 단위로 추적하고, 브랜치(branch)를 생성하거나, 서로 다른 브랜치를 병합(merge) 할 수 있는 강력한 기능을 제공합니다.

+Docsa는 문서의 변경 사항을 기록(commit) 단위로 추적하고, 버전(branch) 를 분기하거나, 서로 다른 버전을 병합(merge) 할 수 있는 강력한 기능을 제공합니다.

이러한 기능은 editor.js 기반의 블록 단위 저장 방식을 통해 구현되며, 변경된 블록만을 감지하여 데이터베이스에 저장하고 이를 조합하여 기록함으로써 저장 효율성과 자원 활용도를 극대화합니다. ## 📌 주요 기능 >*Git의 개념과 용어를 모르는 일반인 유저를 위해, Docsa에서는 일반적인 Git의 개념과 대응되는 용어를 다음과 같이 재정의합니다.
->(기록: commit, 브랜치: branch, 병합하기: merge) +>(기록: commit, 버전: branch, 병합하기: merge) -### 문서 브랜치 관리 +### 문서 버전 관리 -- 문서는 기록, 저장과 브랜치를 가진 최상위 단위입니다. 문서마다 모든 변경 사항을 기록 단위로 저장하여, 원하는 시점의 기록을 조회(checkout) 할 수 있으며, 메인화면에서 생성과 삭제가 가능합니다. +- 문서는 기록,저장과 버전을 가진 최상위 단위입니다. 문서마다 모든 변경 사항을 기록 단위로 저장하여, 원하는 시점의 기록을 조회 (checkout) 할 수 있으며, 메인화면에서 생성과 삭제가 가능합니다. -### 브랜치(branch) 생성 및 병합 +### 버전(branch) 분기 및 병합 -- 하나의 문서에서 여러 브랜치를 생성하여 자유롭게 작업을 분기할 수 있으며, 브랜치 간 병합을 통해 작업 내용을 통합할 수 있습니다. main 브랜치 이외 브랜치 단위의 삭제도 가능합니다. +- 하나의 문서에서 여러 버전을 생성하여 자유롭게 분기해나갈 수 있으며, 브랜치 간 병합을 통해 작업 내용을 통합할 수 있습니다. main버전 이외 버전 단위의 삭제도 가능합니다. ### 저장(Save) 및 기록(Commit) 시스템 @@ -32,9 +32,9 @@ Docsa는 문서의 변경 사항을 기록(commit) 단위로 추적하고, 브 - ‘비교하기’를 통해 현재 보고있는 기록과 다른 기록을 문단 단위로 비교할 수 있습니다. -### 시각화된 브랜치 그래프 UI +### 시각화된 버전 그래프 UI -- 기록과 저장, 브랜치 간의 관계를 트리 구조 그래프로 시각화하여, 문서의 브랜치 흐름을 직관적으로 확인할 수 있습니다. +- 기록과 저장,브랜치 간의 관계를 트리 구조 그래프로 시각화하여, 문서의 버전 흐름을 직관적으로 확인할 수 있습니다. ### 실시간 문서 편집 @@ -156,3 +156,4 @@ Docsa는 문서의 변경 사항을 기록(commit) 단위로 추적하고, 브 + diff --git a/build.gradle b/build.gradle index f9ce7be2..a5e9281f 100644 --- a/build.gradle +++ b/build.gradle @@ -45,22 +45,24 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework:spring-aspects' + implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springframework.boot:spring-boot-starter-actuator' compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.awaitility:awaitility:4.2.0' implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.12.1' - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-mysql' - implementation platform('software.amazon.awssdk:bom:2.42.35') implementation 'software.amazon.awssdk:s3' diff --git a/infra/.env.example b/infra/.env.example index dd100af7..fb66b2f1 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -30,16 +30,3 @@ MYSQL_ROOT_PASSWORD= # ------------ GRAFANA ------------ GRAFANA_ADMIN= GRAFANA_PASSWORD= - -# ------------ AWS ------------ -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= - -AWS_BUCKET= -AWS_PRESIGNED_EXPIRE_MIN= -AWS_REGION= -CDN_URL= - -# ------------ FLYWAY ------------ -FLYWAY_BASELINE_ON_MIGRATE= -FLYWAY_BASELINE_VERSION= \ No newline at end of file diff --git a/infra/docker-compose.stg.yml b/infra/docker-compose.stg.yml index 5c27d46b..66cff9bd 100644 --- a/infra/docker-compose.stg.yml +++ b/infra/docker-compose.stg.yml @@ -136,12 +136,6 @@ services: image: prom/prometheus:latest container_name: prometheus-stg restart: unless-stopped - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--web.enable-remote-write-receiver" - ports: - - "${PROMETHEUS_BIND_ADDR:-127.0.0.1}:9090:9090" depends_on: - cadvisor volumes: diff --git a/perf/delete/README.md b/perf/delete/README.md index bcd8945c..fc0c9ef7 100644 --- a/perf/delete/README.md +++ b/perf/delete/README.md @@ -8,8 +8,8 @@ ## Files -- `perf/seed/seed_dataset.js` - - 유저별 문서/커밋/브랜치 데이터 생성. delete/read/thumbnail E2E에서 공용으로 사용 +- `perf/delete/seed_dataset.js` + - 유저별 문서/커밋/브랜치 데이터 생성 - `perf/delete/delete_only_benchmark.js` - 생성된 문서를 찾아 삭제만 수행 - `perf/delete/compare_delete_summary.mjs` @@ -69,7 +69,7 @@ USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \ USER_COUNT=100 DOCS_PER_USER=3 \ MAIN_COMMITS=6 FEATURE_COMMITS=4 BLOCKS_PER_COMMIT=20 \ SEED_VUS=20 \ -k6 run perf/seed/seed_dataset.js +k6 run perf/delete/seed_dataset.js ``` 생성 규칙: @@ -103,7 +103,7 @@ k6 run perf/delete/delete_only_benchmark.js \ 1. 브랜치 checkout 2. 서버 실행 3. 유저 준비(`PERF_SEED_USER_COUNT`, `PERF_SEED_DOCS_PER_USER=0` 적용된 상태) -4. 데이터 생성 (`perf/seed/seed_dataset.js`) +4. 데이터 생성 (`seed_dataset.js`) 5. 삭제 벤치 실행 (`delete_only_benchmark.js`) 6. 결과 파일 저장 diff --git a/perf/seed/seed_dataset.js b/perf/delete/seed_dataset.js similarity index 97% rename from perf/seed/seed_dataset.js rename to perf/delete/seed_dataset.js index d59b3b6b..ec61336a 100644 --- a/perf/seed/seed_dataset.js +++ b/perf/delete/seed_dataset.js @@ -13,7 +13,6 @@ const MAIN_COMMITS = Number(__ENV.MAIN_COMMITS || 6); const FEATURE_COMMITS = Number(__ENV.FEATURE_COMMITS || 4); const BLOCKS_PER_COMMIT = Number(__ENV.BLOCKS_PER_COMMIT || 20); const RUN_ID = __ENV.RUN_ID || Math.floor(Date.now() / 1000).toString(36); -const RESULT_DIR = __ENV.RESULT_DIR || ''; const TOTAL_DOCS = USER_COUNT * DOCS_PER_USER; @@ -221,13 +220,8 @@ export default function () { } export function handleSummary(data) { - const summary = { + return { stdout: `\n[seed-dataset] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, total_docs=${TOTAL_DOCS}\n`, + 'perf/delete/results/seed_dataset_summary.json': JSON.stringify(data, null, 2), }; - - if (RESULT_DIR) { - summary[`${RESULT_DIR}/seed_dataset_summary.json`] = JSON.stringify(data, null, 2); - } - - return summary; } diff --git a/perf/read/README.md b/perf/read/README.md index e635a21b..794e751b 100644 --- a/perf/read/README.md +++ b/perf/read/README.md @@ -8,7 +8,7 @@ ## Files -- `perf/seed/seed_dataset.js` +- `perf/delete/seed_dataset.js` - 목록 테스트에 사용할 문서/브랜치/커밋 데이터를 생성 - `perf/read/doc_list_benchmark.js` - `sidebar`, `full list`, `search` 읽기 성능 측정 @@ -35,7 +35,7 @@ PERF_SEED_DOCS_PER_USER=0 ## 2) Seed dataset -공용 seed 스크립트를 사용합니다. +삭제 벤치에서 쓰던 seed 스크립트를 그대로 재사용합니다. 중요한 건 목록 조회 시 문서별 branch/commit이 충분히 많아지도록 데이터를 만드는 것입니다. ```bash @@ -45,7 +45,7 @@ USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \ USER_COUNT=50 DOCS_PER_USER=5 \ MAIN_COMMITS=8 FEATURE_COMMITS=5 BLOCKS_PER_COMMIT=100 \ SEED_VUS=20 \ -k6 run perf/seed/seed_dataset.js +k6 run perf/delete/seed_dataset.js ``` 애플리케이션 initializer로 DB에 직접 주입할 수도 있습니다. 이 방식은 API 호출 비용 없이 @@ -89,7 +89,7 @@ SEARCH_PREFIX=PERF 결과 파일: -- `perf/read/results//.../*.json` +- `perf/read/results/doc_list_benchmark_.json` 핵심 지표: diff --git a/perf/read/doc_list_benchmark.js b/perf/read/doc_list_benchmark.js index 952eaeb8..bb40bd7d 100644 --- a/perf/read/doc_list_benchmark.js +++ b/perf/read/doc_list_benchmark.js @@ -11,7 +11,6 @@ const USER_COUNT = Number(__ENV.USER_COUNT || 50); const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 3); const PAGE_SIZE = Number(__ENV.PAGE_SIZE || 10); const RUN_ID = __ENV.RUN_ID; -const DATASET_RUN_ID = __ENV.DATASET_RUN_ID || RUN_ID; const RESULT_DIR = __ENV.RESULT_DIR || 'perf/read/results'; const SEARCH_PREFIX = __ENV.SEARCH_PREFIX || 'PDEL'; @@ -20,7 +19,6 @@ if (!RUN_ID) { } export const options = { - summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'max'], scenarios: { sidebar_list: { executor: 'constant-vus', @@ -72,9 +70,9 @@ function userEmail(userNo) { function keywordForUser(userNo) { if (SEARCH_PREFIX === 'PERF') { - return `PERF-${DATASET_RUN_ID}-u${pad3(userNo)}`; + return `PERF-${RUN_ID}-u${pad3(userNo)}`; } - return `${SEARCH_PREFIX}-${DATASET_RUN_ID}u${pad3(userNo)}`; + return `${SEARCH_PREFIX}-${RUN_ID}u${pad3(userNo)}`; } function headers(cookie) { diff --git a/perf/thumbnail/preview_e2e_benchmark.js b/perf/thumbnail/preview_e2e_benchmark.js deleted file mode 100644 index 6d70e127..00000000 --- a/perf/thumbnail/preview_e2e_benchmark.js +++ /dev/null @@ -1,194 +0,0 @@ -import http from 'k6/http'; -import exec from 'k6/execution'; -import { check } from 'k6'; -import { Rate, Trend } from 'k6/metrics'; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; -const USER_PREFIX = __ENV.USER_PREFIX || 'perfe2e'; -const USER_DOMAIN = __ENV.USER_DOMAIN || 'test.com'; -const USER_PASSWORD = __ENV.USER_PASSWORD || 'Testtest1'; -const USER_COUNT = Number(__ENV.USER_COUNT || 1); -const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 20); -const RUN_ID = __ENV.RUN_ID || 'e2e_u1d20'; -const E2E_VUS = Number(__ENV.E2E_VUS || 1); -const E2E_DURATION = __ENV.E2E_DURATION || '30s'; -const RESULT_DIR = __ENV.RESULT_DIR || 'perf/thumbnail/results'; - -export const options = { - summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'max'], - scenarios: { - preview_e2e: { - executor: 'constant-vus', - vus: E2E_VUS, - duration: E2E_DURATION, - exec: 'runPreviewE2E', - }, - }, - thresholds: { - http_req_failed: ['rate<0.02'], - preview_e2e_failed: ['rate<0.02'], - }, -}; - -const e2eFailed = new Rate('preview_e2e_failed'); -const tLogin = new Trend('preview_login_ms'); -const tSetupDocList = new Trend('preview_setup_doc_list_ms'); -const tSetupGraph = new Trend('preview_setup_graph_ms'); -const tSaveUpdate = new Trend('op_preview_save_update_ms'); -const tDocList = new Trend('op_preview_doc_list_ms'); -const tTotal = new Trend('op_preview_e2e_total_ms'); - -function pad3(n) { - return String(n).padStart(3, '0'); -} - -function userEmail(userNo) { - return `${USER_PREFIX}_u${pad3(userNo)}@${USER_DOMAIN}`; -} - -function headers(cookie) { - return { - headers: { - Cookie: cookie, - 'Content-Type': 'application/json', - }, - }; -} - -function ensureStatus(res, statuses, op) { - const ok = check(res, { - [`${op} status`]: (r) => statuses.includes(r.status), - }); - e2eFailed.add(!ok, { op }); - if (!ok) { - const snippet = typeof res.body === 'string' ? res.body.slice(0, 300) : ''; - throw new Error(`${op} failed status=${res.status}, body=${snippet}`); - } -} - -function parseJson(res, op) { - try { - return res.json(); - } catch (_e) { - throw new Error(`${op} invalid JSON`); - } -} - -function login(email) { - const loginJar = new http.CookieJar(); - const res = http.post( - `${BASE_URL}/api/user/login`, - JSON.stringify({ email, password: USER_PASSWORD }), - { - headers: { 'Content-Type': 'application/json' }, - jar: loginJar, - tags: { op: 'preview_login', name: 'POST /api/user/login' }, - }, - ); - tLogin.add(res.timings.duration); - ensureStatus(res, [200], 'preview_login'); - - const jsession = res.cookies.JSESSIONID && res.cookies.JSESSIONID[0]; - if (!jsession || !jsession.value) { - throw new Error('preview_login missing JSESSIONID'); - } - return `JSESSIONID=${jsession.value}`; -} - -function fetchDocs(cookie) { - const res = http.get( - `${BASE_URL}/api/document?page=0&size=${DOCS_PER_USER}&sort=updatedAt&order=desc`, - { headers: { Cookie: cookie }, tags: { op: 'preview_setup_doc_list', name: 'GET /api/document' } }, - ); - tSetupDocList.add(res.timings.duration); - ensureStatus(res, [200], 'preview_setup_doc_list'); - const body = parseJson(res, 'preview_setup_doc_list'); - if (!body || !Array.isArray(body.content) || body.content.length === 0) { - throw new Error('preview_setup_doc_list empty content'); - } - return body.content.map((doc) => ({ docId: doc.id })); -} - -function attachSaveIds(cookie, docs) { - return docs.map((doc) => { - const res = http.get( - `${BASE_URL}/api/document/${doc.docId}/graph`, - { headers: { Cookie: cookie }, tags: { op: 'preview_setup_graph', name: 'GET /api/document/{docId}/graph' } }, - ); - tSetupGraph.add(res.timings.duration); - ensureStatus(res, [200], 'preview_setup_graph'); - const graph = parseJson(res, 'preview_setup_graph'); - const branch = graph.branches && graph.branches.find((item) => item.saveId); - if (!branch) { - throw new Error(`preview_setup_graph missing saveId docId=${doc.docId}`); - } - return { docId: doc.docId, saveId: branch.saveId }; - }); -} - -function buildSaveContent(docId, iteration) { - return [ - { - id: `preview-e2e-${docId}-${iteration}-1`, - type: 'paragraph', - props: {}, - content: [ - { - type: 'text', - text: `preview e2e ${RUN_ID} doc ${docId} iteration ${iteration}`, - styles: {}, - }, - ], - children: [], - }, - ]; -} - -function requestSaveUpdate(cookie, target, iteration) { - const res = http.put( - `${BASE_URL}/api/document/${target.docId}/save/${target.saveId}`, - JSON.stringify({ content: buildSaveContent(target.docId, iteration) }), - { ...headers(cookie), tags: { op: 'preview_save_update', name: 'PUT /api/document/{docId}/save/{saveId}' } }, - ); - tSaveUpdate.add(res.timings.duration); - ensureStatus(res, [200], 'preview_save_update'); -} - -function requestDocList(cookie) { - const res = http.get( - `${BASE_URL}/api/document?page=0&size=${DOCS_PER_USER}&sort=updatedAt&order=desc`, - { headers: { Cookie: cookie }, tags: { op: 'preview_doc_list', name: 'GET /api/document' } }, - ); - tDocList.add(res.timings.duration); - ensureStatus(res, [200], 'preview_doc_list'); -} - -export function setup() { - const users = []; - for (let userNo = 1; userNo <= USER_COUNT; userNo += 1) { - const cookie = login(userEmail(userNo)); - const docs = attachSaveIds(cookie, fetchDocs(cookie)); - users.push({ userNo, cookie, docs }); - } - return { users }; -} - -export function runPreviewE2E(data) { - const userIndex = (exec.vu.idInTest - 1) % data.users.length; - const user = data.users[userIndex]; - const iteration = exec.scenario.iterationInTest; - const target = user.docs[(iteration + exec.vu.idInTest - 1) % user.docs.length]; - const started = Date.now(); - - requestSaveUpdate(user.cookie, target, iteration); - requestDocList(user.cookie); - - tTotal.add(Date.now() - started); -} - -export function handleSummary(data) { - return { - stdout: `\n[preview-e2e] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, vus=${E2E_VUS}, duration=${E2E_DURATION}\n`, - [`${RESULT_DIR}/preview_e2e_${RUN_ID}.json`]: JSON.stringify(data, null, 2), - }; -} diff --git a/perf/thumbnail/thumbnail_workflow_benchmark.js b/perf/thumbnail/thumbnail_workflow_benchmark.js deleted file mode 100644 index 534d39ce..00000000 --- a/perf/thumbnail/thumbnail_workflow_benchmark.js +++ /dev/null @@ -1,300 +0,0 @@ -import http from 'k6/http'; -import exec from 'k6/execution'; -import { check } from 'k6'; -import { Rate, Trend } from 'k6/metrics'; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; -const USER_PREFIX = __ENV.USER_PREFIX || 'perfthumb'; -const USER_DOMAIN = __ENV.USER_DOMAIN || 'test.com'; -const USER_PASSWORD = __ENV.USER_PASSWORD || 'Testtest1'; -const USER_COUNT = Number(__ENV.USER_COUNT || 5); -const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 5); -const RUN_ID = __ENV.RUN_ID || 'thumbwf'; -const WORKFLOW_VUS = Number(__ENV.WORKFLOW_VUS || USER_COUNT); -const WORKFLOW_DURATION = __ENV.WORKFLOW_DURATION || '30s'; -const RESULT_DIR = __ENV.RESULT_DIR || 'perf/thumbnail/results'; -const THUMBNAIL_CONTENT_TYPE = __ENV.THUMBNAIL_CONTENT_TYPE || 'image/webp'; -const THUMBNAIL_SIZE_BYTES = Number(__ENV.THUMBNAIL_SIZE_BYTES || 1024); -const THUMBNAIL_BODY = 'x'.repeat(THUMBNAIL_SIZE_BYTES); - -if (WORKFLOW_VUS > USER_COUNT) { - throw new Error('WORKFLOW_VUS must be <= USER_COUNT to avoid same-user thumbnail token contention'); -} - -export const options = { - summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'max'], - scenarios: { - thumbnail_workflow: { - executor: 'constant-vus', - vus: WORKFLOW_VUS, - duration: WORKFLOW_DURATION, - exec: 'runThumbnailWorkflow', - }, - }, - thresholds: { - http_req_failed: ['rate<0.02'], - thumbnail_workflow_failed: ['rate<0.02'], - }, -}; - -const workflowFailed = new Rate('thumbnail_workflow_failed'); -const tLogin = new Trend('thumbnail_login_ms'); -const tDocListSetup = new Trend('thumbnail_setup_doc_list_ms'); -const tGraphSetup = new Trend('thumbnail_setup_graph_ms'); -const tSaveUpdate = new Trend('op_save_update_ms'); -const tUploadUrl = new Trend('op_image_upload_url_ms'); -const tS3PutThumbnail = new Trend('op_s3_put_thumbnail_ms'); -const tImageComplete = new Trend('op_image_complete_ms'); -const tThumbnailFinalize = new Trend('op_thumbnail_finalize_ms'); -const tDocListAfterThumbnail = new Trend('op_doc_list_after_thumbnail_ms'); -const tWorkflowTotal = new Trend('op_thumbnail_workflow_total_ms'); - -function pad3(n) { - return String(n).padStart(3, '0'); -} - -function userEmail(userNo) { - return `${USER_PREFIX}_u${pad3(userNo)}@${USER_DOMAIN}`; -} - -function headers(cookie) { - return { - headers: { - Cookie: cookie, - 'Content-Type': 'application/json', - }, - }; -} - -function ensureStatus(res, statuses, op) { - const ok = check(res, { - [`${op} status`]: (r) => statuses.includes(r.status), - }); - workflowFailed.add(!ok, { op }); - if (!ok) { - const snippet = typeof res.body === 'string' ? res.body.slice(0, 300) : ''; - throw new Error(`${op} failed status=${res.status}, body=${snippet}`); - } -} - -function parseJson(res, op) { - try { - return res.json(); - } catch (_e) { - throw new Error(`${op} invalid JSON`); - } -} - -function login(email) { - const loginJar = new http.CookieJar(); - const res = http.post( - `${BASE_URL}/api/user/login`, - JSON.stringify({ email, password: USER_PASSWORD }), - { - headers: { 'Content-Type': 'application/json' }, - jar: loginJar, - tags: { op: 'thumbnail_login', name: 'POST /api/user/login' }, - }, - ); - tLogin.add(res.timings.duration); - ensureStatus(res, [200], 'thumbnail_login'); - - const jsession = res.cookies.JSESSIONID && res.cookies.JSESSIONID[0]; - if (!jsession || !jsession.value) { - throw new Error('thumbnail_login missing JSESSIONID'); - } - return `JSESSIONID=${jsession.value}`; -} - -function fetchDocs(cookie) { - const res = http.get( - `${BASE_URL}/api/document?page=0&size=${DOCS_PER_USER}&sort=updatedAt&order=desc`, - { headers: { Cookie: cookie }, tags: { op: 'thumbnail_setup_doc_list', name: 'GET /api/document' } }, - ); - tDocListSetup.add(res.timings.duration); - ensureStatus(res, [200], 'thumbnail_setup_doc_list'); - const body = parseJson(res, 'thumbnail_setup_doc_list'); - if (!body || !Array.isArray(body.content) || body.content.length === 0) { - throw new Error('thumbnail_setup_doc_list empty content'); - } - return body.content.map((doc) => ({ docId: doc.id })); -} - -function attachSaveIds(cookie, docs) { - return docs.map((doc) => { - const res = http.get( - `${BASE_URL}/api/document/${doc.docId}/graph`, - { headers: { Cookie: cookie }, tags: { op: 'thumbnail_setup_graph', name: 'GET /api/document/{docId}/graph' } }, - ); - tGraphSetup.add(res.timings.duration); - ensureStatus(res, [200], 'thumbnail_setup_graph'); - const graph = parseJson(res, 'thumbnail_setup_graph'); - const branch = graph.branches && graph.branches.find((item) => item.saveId); - if (!branch) { - throw new Error(`thumbnail_setup_graph missing saveId docId=${doc.docId}`); - } - return { docId: doc.docId, saveId: branch.saveId }; - }); -} - -function buildSaveContent(docId, iteration) { - return [ - { - id: `thumbwf-${docId}-${iteration}-1`, - type: 'paragraph', - props: {}, - content: [ - { - type: 'text', - text: `thumbnail workflow ${RUN_ID} doc ${docId} iteration ${iteration}`, - styles: {}, - }, - ], - children: [], - }, - ]; -} - -function requestSaveUpdate(cookie, target, iteration) { - const res = http.put( - `${BASE_URL}/api/document/${target.docId}/save/${target.saveId}`, - JSON.stringify({ content: buildSaveContent(target.docId, iteration) }), - { ...headers(cookie), tags: { op: 'save_update', name: 'PUT /api/document/{docId}/save/{saveId}' } }, - ); - tSaveUpdate.add(res.timings.duration); - ensureStatus(res, [200], 'save_update'); - const body = parseJson(res, 'save_update'); - if (!body.thumbnail || !body.thumbnail.requestToken) { - throw new Error('save_update missing thumbnail.requestToken'); - } - return body.thumbnail.requestToken; -} - -function requestUploadUrl(cookie, docId, iteration) { - const res = http.post( - `${BASE_URL}/api/images/upload-url`, - JSON.stringify({ - docId, - originalFileName: `thumbnail-${RUN_ID}-${docId}-${iteration}.${extensionOf(THUMBNAIL_CONTENT_TYPE)}`, - contentType: THUMBNAIL_CONTENT_TYPE, - size: THUMBNAIL_SIZE_BYTES, - purpose: 'DOC_THUMBNAIL', - }), - { ...headers(cookie), tags: { op: 'image_upload_url', name: 'POST /api/images/upload-url' } }, - ); - tUploadUrl.add(res.timings.duration); - ensureStatus(res, [200], 'image_upload_url'); - const body = parseJson(res, 'image_upload_url'); - if (!body.imageId || !body.uploadUrl) { - throw new Error('image_upload_url missing imageId or uploadUrl'); - } - return { - imageId: body.imageId, - uploadUrl: body.uploadUrl, - method: body.method || 'PUT', - }; -} - -function extensionOf(contentType) { - switch (contentType) { - case 'image/jpeg': - return 'jpg'; - case 'image/png': - return 'png'; - case 'image/webp': - return 'webp'; - case 'image/gif': - return 'gif'; - default: - return 'bin'; - } -} - -function requestS3PutThumbnail(upload) { - if (upload.method !== 'PUT') { - throw new Error(`s3_put_thumbnail unsupported upload method=${upload.method}`); - } - - const res = http.put( - upload.uploadUrl, - THUMBNAIL_BODY, - { - headers: { - 'Content-Type': THUMBNAIL_CONTENT_TYPE, - }, - tags: { - op: 's3_put_thumbnail', - name: 's3_put_thumbnail', - }, - }, - ); - tS3PutThumbnail.add(res.timings.duration); - ensureStatus(res, [200, 201, 204], 's3_put_thumbnail'); -} - -function requestImageComplete(cookie, imageId) { - const res = http.post( - `${BASE_URL}/api/images/${imageId}/complete`, - null, - { headers: { Cookie: cookie }, tags: { op: 'image_complete', name: 'POST /api/images/{imageId}/complete' } }, - ); - tImageComplete.add(res.timings.duration); - ensureStatus(res, [200], 'image_complete'); -} - -function requestThumbnailFinalize(cookie, target, imageId, requestToken, iteration) { - const res = http.put( - `${BASE_URL}/api/document/${target.docId}/thumbnail`, - JSON.stringify({ - imageId, - requestToken, - signature: `sig-${RUN_ID}-${target.docId}-${iteration}`, - }), - { ...headers(cookie), tags: { op: 'thumbnail_finalize', name: 'PUT /api/document/{docId}/thumbnail' } }, - ); - tThumbnailFinalize.add(res.timings.duration); - ensureStatus(res, [200], 'thumbnail_finalize'); -} - -function requestDocList(cookie) { - const res = http.get( - `${BASE_URL}/api/document?page=0&size=${DOCS_PER_USER}&sort=updatedAt&order=desc`, - { headers: { Cookie: cookie }, tags: { op: 'doc_list_after_thumbnail', name: 'GET /api/document' } }, - ); - tDocListAfterThumbnail.add(res.timings.duration); - ensureStatus(res, [200], 'doc_list_after_thumbnail'); -} - -export function setup() { - const users = []; - for (let userNo = 1; userNo <= USER_COUNT; userNo += 1) { - const cookie = login(userEmail(userNo)); - const docs = attachSaveIds(cookie, fetchDocs(cookie)); - users.push({ userNo, cookie, docs }); - } - return { users }; -} - -export function runThumbnailWorkflow(data) { - const userIndex = (exec.vu.idInTest - 1) % data.users.length; - const user = data.users[userIndex]; - const iteration = exec.scenario.iterationInTest; - const target = user.docs[iteration % user.docs.length]; - const started = Date.now(); - - const requestToken = requestSaveUpdate(user.cookie, target, iteration); - const upload = requestUploadUrl(user.cookie, target.docId, iteration); - requestS3PutThumbnail(upload); - requestImageComplete(user.cookie, upload.imageId); - requestThumbnailFinalize(user.cookie, target, upload.imageId, requestToken, iteration); - requestDocList(user.cookie); - - tWorkflowTotal.add(Date.now() - started); -} - -export function handleSummary(data) { - return { - stdout: `\n[thumbnail-workflow] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, vus=${WORKFLOW_VUS}, duration=${WORKFLOW_DURATION}, s3_put=included, thumbnail_size=${THUMBNAIL_SIZE_BYTES}\n`, - [`${RESULT_DIR}/thumbnail_workflow_${RUN_ID}.json`]: JSON.stringify(data, null, 2), - }; -} diff --git a/src/main/java/io/ejangs/docsa/DocsaApplication.java b/src/main/java/io/ejangs/docsa/DocsaApplication.java index 30409793..8f723cb6 100644 --- a/src/main/java/io/ejangs/docsa/DocsaApplication.java +++ b/src/main/java/io/ejangs/docsa/DocsaApplication.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.info.Info; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -15,6 +16,7 @@ ) ) @EnableAsync +@EnableRetry @EnableScheduling @SpringBootApplication public class DocsaApplication { diff --git a/src/main/java/io/ejangs/docsa/domain/branch/app/BranchService.java b/src/main/java/io/ejangs/docsa/domain/branch/app/BranchService.java index d419599d..75751f1a 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/app/BranchService.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/app/BranchService.java @@ -16,12 +16,12 @@ import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; -import io.ejangs.docsa.global.outbox.mongo.util.MongoDeleteMapper; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.util.MongoDeleteMapper; import io.ejangs.docsa.global.util.RenewUpdatedAtHelper; import java.util.*; diff --git a/src/main/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestrator.java b/src/main/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestrator.java index 3ebfe126..55f9693d 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestrator.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestrator.java @@ -4,11 +4,11 @@ import io.ejangs.docsa.domain.branch.dto.response.BranchCreateResponse; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/ejangs/docsa/domain/branch/merge/app/MergeOrchestrator.java b/src/main/java/io/ejangs/docsa/domain/branch/merge/app/MergeOrchestrator.java index 232dfe32..f94086d7 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/merge/app/MergeOrchestrator.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/merge/app/MergeOrchestrator.java @@ -7,11 +7,11 @@ import io.ejangs.docsa.domain.branch.merge.dto.response.MergeResponse; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.CommitErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/ejangs/docsa/domain/branch/merge/swagger/MergeDocs.java b/src/main/java/io/ejangs/docsa/domain/branch/merge/swagger/MergeDocs.java index 89aa5e0e..723a581b 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/merge/swagger/MergeDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/merge/swagger/MergeDocs.java @@ -140,7 +140,7 @@ value = """ { "status": 400, - "message": "새로운 브랜치의 이름은 다른 브랜치의 이름과 중복될 수 없습니다.", + "message": "새로운 분기의 이름은 다른 버전의 이름과 중복될 수 없습니다.", "error": "BRANCH_NAME_DUPLICATED" } """ diff --git a/src/main/java/io/ejangs/docsa/domain/branch/swagger/CreateBranchDocs.java b/src/main/java/io/ejangs/docsa/domain/branch/swagger/CreateBranchDocs.java index e530c2ad..f5bd80fe 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/swagger/CreateBranchDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/swagger/CreateBranchDocs.java @@ -98,7 +98,7 @@ value = """ { "status": 400, - "message": "새로운 브랜치의 이름은 다른 브랜치의 이름과 중복될 수 없습니다.", + "message": "새로운 버전의 이름은 다른 버전의 이름과 중복될 수 없습니다.", "error": "BRANCH_NAME_DUPLICATED" } """ diff --git a/src/main/java/io/ejangs/docsa/domain/branch/swagger/DeleteBranchDocs.java b/src/main/java/io/ejangs/docsa/domain/branch/swagger/DeleteBranchDocs.java index 40e5738c..3350a9ae 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/swagger/DeleteBranchDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/swagger/DeleteBranchDocs.java @@ -18,12 +18,12 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Operation( - summary = "브랜치 삭제", + summary = "버전 삭제", description = """ - 브랜치를 삭제합니다. 삭제 조건은 아래와 같습니다: + 버전을 삭제합니다. 삭제 조건은 아래와 같습니다: - - 메인 브랜치(`fromCommit == null`)는 삭제 불가 - - 다른 브랜치가 이 브랜치를 기반(fromCommit)으로 만들어졌다면 삭제 불가 + - 메인 버전(`fromCommit == null`)는 삭제 불가 + - 다른 버전이 이 버전을 기반(fromCommit)으로 만들어졌다면 삭제 불가 - 블록, 시퀀스, 저장(MongoDB)도 함께 삭제됨 """, parameters = { @@ -36,7 +36,7 @@ ), @Parameter( name = "branchId", - description = "삭제할 브랜치 ID", + description = "삭제할 버전 ID", example = "5", required = true, in = ParameterIn.PATH @@ -45,31 +45,31 @@ responses = { @ApiResponse( responseCode = "204", - description = "브랜치 삭제 성공" + description = "버전 삭제 성공" ), @ApiResponse( responseCode = "400", - description = "브랜치 삭제 실패 - 삭제 불가능한 브랜치", + description = "버전 삭제 실패 - 삭제 불가능한 버전", content = @Content( schema = @Schema(implementation = ErrorResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { @ExampleObject( - name = "메인 브랜치 삭제 시도", + name = "메인 버전 삭제 시도", value = """ { "status": 400, - "message": "메인 브랜치는 삭제 또는 수정할 수 없습니다.", + "message": "기본 버전은 삭제 또는 수정할 수 없습니다.", "error": "MAIN_BRANCH_FIX_UNAVAILABLE" } """ ), @ExampleObject( - name = "파생된 브랜치가 존재", + name = "파생된 버전이 존재", value = """ { "status": 400, - "message": "서브 브랜치가 있어 해당 브랜치를 삭제할 수 없습니다.", + "message": "서브 버전이 있어 해당 버전을 삭제할 수 없습니다.", "error": "SUB_BRANCH_DELETE_UNAVAILABLE" } """ @@ -96,7 +96,7 @@ ), @ApiResponse( responseCode = "404", - description = "브랜치 삭제 실패 - 문서 또는 브랜치가 존재하지 않음", + description = "버전 삭제 실패 - 문서 또는 버전이 존재하지 않음", content = @Content( schema = @Schema(implementation = ErrorResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -104,7 +104,7 @@ value = """ { "status": 404, - "message": "해당 브랜치를 찾을 수 없습니다.", + "message": "해당 버전을 찾을 수 없습니다.", "error": "BRANCH_NOT_FOUND" } """ @@ -113,7 +113,7 @@ ), @ApiResponse( responseCode = "500", - description = "브랜치 삭제 실패 - MySQL 또는 MongoDB 저장 실패", + description = "버전 삭제 실패 - MySQL 또는 MongoDB 저장 실패", content = @Content( schema = @Schema(implementation = ErrorResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, diff --git a/src/main/java/io/ejangs/docsa/domain/branch/swagger/RenameBranchDocs.java b/src/main/java/io/ejangs/docsa/domain/branch/swagger/RenameBranchDocs.java index da8f0c38..19a16d6a 100644 --- a/src/main/java/io/ejangs/docsa/domain/branch/swagger/RenameBranchDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/branch/swagger/RenameBranchDocs.java @@ -20,19 +20,19 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Operation( - summary = "브랜치 이름 변경", - description = "브랜치의 이름을 수정합니다. 메인 브랜치의 이름은 수정할 수 없습니다.", + summary = "버전 이름 변경", + description = "버전의 이름을 수정합니다. 메인버전의 이름은 수정할 수 없습니다.", parameters = { @Parameter( name = "docId", - description = "수정하려는 브랜치가 속한 문서 id", + description = "수정하려는 버전이 속한 문서 id", example = "1", required = true, in = ParameterIn.PATH ), @Parameter( name = "branchId", - description = "수정하려는 브랜치의 id", + description = "수정하려는 버전의 id", example = "1", required = true, in = ParameterIn.PATH @@ -43,9 +43,9 @@ schema = @Schema(implementation = BranchRenameRequest.class), mediaType = MediaType.APPLICATION_JSON_VALUE, examples = @ExampleObject( - value = """ + value = """ { - "newName" : "수정한 브랜치 이름" + "newName" : "수정한 버전 이름" } """ ) @@ -54,7 +54,7 @@ responses = { @ApiResponse( responseCode = "200", - description = "브랜치 이름 수정 성공", + description = "버전 이름 수정 성공", content = @Content( schema = @Schema(implementation = BranchRenameResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -62,7 +62,7 @@ value = """ { "id" : 1, - "name": "수정한 브랜치 이름" + "name": "수정한 버전 이름" } """ ) @@ -70,7 +70,7 @@ ), @ApiResponse( responseCode = "400", - description = "브랜치 이름 수정 실패 - 해당 문서에 속한 브랜치가 아님", + description = "버전 이름 수정 실패 - 해당 문서에 속한 버전이 아님", content = @Content( schema = @Schema(implementation = ErrorResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -79,7 +79,7 @@ value = """ { "status": 400, - "message": "해당 브랜치를 찾을 수 없습니다.", + "message": "해당 버전을 찾을 수 없습니다.", "error": "BRANCH_NOT_FOUND_OR_FORBIDDEN" } """ @@ -106,7 +106,7 @@ ), @ApiResponse( responseCode = "404", - description = "브랜치 이름 수정 실패 - 해당 id를 가진 브랜치 없음", + description = "버전 이름 수정 실패 - 해당 id를 가진 버전 없음", content = @Content( schema = @Schema(implementation = ErrorResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -115,7 +115,7 @@ value = """ { "status": 404, - "message": "해당 브랜치를 찾을 수 없습니다.", + "message": "해당 버전을 찾을 수 없습니다.", "error": "BRANCH_NOT_FOUND" } """ @@ -124,7 +124,7 @@ ), @ApiResponse( responseCode = "500", - description = "브랜치 이름 수정 실패 - MySQL 또는 MongoDB 저장 실패", + description = "버전 이름 수정 실패 - MySQL 또는 MongoDB 저장 실패", content = @Content( schema = @Schema(implementation = ErrorResponse.class), mediaType = MediaType.APPLICATION_JSON_VALUE, diff --git a/src/main/java/io/ejangs/docsa/domain/commit/app/CommitService.java b/src/main/java/io/ejangs/docsa/domain/commit/app/CommitService.java index 46f98244..26ab2b01 100644 --- a/src/main/java/io/ejangs/docsa/domain/commit/app/CommitService.java +++ b/src/main/java/io/ejangs/docsa/domain/commit/app/CommitService.java @@ -13,12 +13,12 @@ import io.ejangs.docsa.domain.doc.entity.Doc; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.CommitErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; -import io.ejangs.docsa.global.outbox.mongo.util.MongoIdsCollector; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.util.MongoIdsCollector; import io.ejangs.docsa.global.util.RenewUpdatedAtHelper; import java.util.List; import java.util.Map; diff --git a/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitCreateOrchestrator.java b/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitCreateOrchestrator.java index 25af5748..516d2ae4 100644 --- a/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitCreateOrchestrator.java +++ b/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitCreateOrchestrator.java @@ -6,11 +6,11 @@ import io.ejangs.docsa.domain.doc.entity.Doc; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.CommitErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMongoTxService.java b/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMongoTxService.java index 5bcb3574..0d7d67c8 100644 --- a/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMongoTxService.java +++ b/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMongoTxService.java @@ -8,7 +8,7 @@ import io.ejangs.docsa.domain.commit.util.CommitBlockSequenceMapper; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BlockSequenceErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; import java.util.ArrayList; import java.util.List; import java.util.Optional; diff --git a/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMySqlTxService.java b/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMySqlTxService.java index 08b4aae4..3afed8fe 100644 --- a/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMySqlTxService.java +++ b/src/main/java/io/ejangs/docsa/domain/commit/app/create/CommitMySqlTxService.java @@ -10,7 +10,7 @@ import io.ejangs.docsa.domain.edge.entity.Edge; import io.ejangs.docsa.domain.edge.util.EdgeMapper; import io.ejangs.docsa.domain.save.app.SaveQueryService; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import io.ejangs.docsa.global.util.RenewUpdatedAtHelper; import java.util.Optional; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/io/ejangs/docsa/domain/commit/swagger/CreateCommitDocs.java b/src/main/java/io/ejangs/docsa/domain/commit/swagger/CreateCommitDocs.java index e050d553..cb602680 100644 --- a/src/main/java/io/ejangs/docsa/domain/commit/swagger/CreateCommitDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/commit/swagger/CreateCommitDocs.java @@ -171,7 +171,7 @@ value = """ { "status": 404, - "message": "해당 브랜치를 찾을 수 없습니다.", + "message": "해당 버전을 찾을 수 없습니다.", "error": "BRANCH_NOT_FOUND_OR_FORBIDDEN" } """ @@ -191,7 +191,7 @@ value = """ { "status": 404, - "message": "해당 브랜치를 찾을 수 없습니다.", + "message": "해당 버전을 찾을 수 없습니다.", "error": "BRANCH_NOT_FOUND" } """ diff --git a/src/main/java/io/ejangs/docsa/domain/commit/swagger/DeleteCommitDocs.java b/src/main/java/io/ejangs/docsa/domain/commit/swagger/DeleteCommitDocs.java index 5e2df56e..d1325003 100644 --- a/src/main/java/io/ejangs/docsa/domain/commit/swagger/DeleteCommitDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/commit/swagger/DeleteCommitDocs.java @@ -25,9 +25,9 @@ 삭제 조건은 아래와 같습니다: - - 각 브랜치의 LeafCommit만 삭제 가능 - - 어느 브랜치의 FromCommit이면 삭제 불가 - - 브랜치의 RootCommit은 삭제 불가(RootCommit까지 삭제하고 싶은 경우 브랜치 삭제를 권장합니다) + - 각 버전의 LeafCommit만 삭제 가능 + - 어느 버전의 FromCommit이면 삭제 불가 + - 버전의 RootCommit은 삭제 불가(RootCommit까지 삭제하고 싶은 경우 버전 삭제를 권장합니다) """, parameters = { @Parameter( @@ -62,7 +62,7 @@ value = """ { "status": 400, - "message": "브랜치의 마지막 기록이 아닙니다.", + "message": "버전의 마지막 기록이 아닙니다.", "error": "IS_NOT_LEAF_COMMIT" } """ diff --git a/src/main/java/io/ejangs/docsa/domain/doc/app/DocService.java b/src/main/java/io/ejangs/docsa/domain/doc/app/DocService.java index 0b739562..e8947f81 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/app/DocService.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/app/DocService.java @@ -24,12 +24,12 @@ import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; -import io.ejangs.docsa.global.outbox.mongo.util.MongoIdsCollector; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.util.MongoIdsCollector; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateMySqlTxService.java b/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateMySqlTxService.java index 906cb674..f25ab6b0 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateMySqlTxService.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateMySqlTxService.java @@ -4,8 +4,6 @@ import io.ejangs.docsa.domain.branch.entity.Branch; import io.ejangs.docsa.domain.doc.dto.response.DocCreateResponse; import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.dao.ThumbnailRepository; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; import io.ejangs.docsa.domain.doc.util.DocMapper; import io.ejangs.docsa.domain.save.app.SaveQueryService; import io.ejangs.docsa.domain.save.entity.Save; @@ -22,7 +20,6 @@ public class DocCreateMySqlTxService { private final DocQueryService docQueryService; private final BranchQueryService branchQueryService; private final SaveQueryService saveQueryService; - private final ThumbnailRepository thumbnailRepository; @Value("${default.branch}") private String defaultBranchName; @@ -34,10 +31,6 @@ public DocCreateResponse createMySqlPart(String title, User user, Doc doc = docQueryService.create(user, title); Branch defaultBranch = branchQueryService.createBranch(doc, defaultBranchName); Save defaultSave = saveQueryService.createSave(defaultBranch, saveContentId); - thumbnailRepository.save(Thumbnail.builder() - .doc(doc) - .build()); - return DocMapper.toCreateResponse(doc, defaultSave); } diff --git a/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateOrchestrator.java b/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateOrchestrator.java index b5bc52c9..21cf7882 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateOrchestrator.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/app/create/DocCreateOrchestrator.java @@ -6,11 +6,11 @@ import io.ejangs.docsa.domain.user.entity.User; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/ejangs/docsa/domain/doc/dto/response/DocPageResponse.java b/src/main/java/io/ejangs/docsa/domain/doc/dto/response/DocPageResponse.java index 41ea9a4d..a5be7626 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/dto/response/DocPageResponse.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/dto/response/DocPageResponse.java @@ -1,7 +1,6 @@ package io.ejangs.docsa.domain.doc.dto.response; import io.ejangs.docsa.domain.doc.dto.RecentActivityDto; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; import java.time.LocalDateTime; public record DocPageResponse( @@ -9,13 +8,17 @@ public record DocPageResponse( String title, LocalDateTime createdAt, LocalDateTime updatedAt, - String thumbnailUrl, - ThumbnailStatus thumbnailStatus, + String preview, RecentActivityDto recent ) { - public DocPageResponse { - createdAt = createdAt.plusHours(9L); - updatedAt = updatedAt.plusHours(9L); + public DocPageResponse(Long id, String title, LocalDateTime createdAt, + LocalDateTime updatedAt, String preview, RecentActivityDto recent) { + this.id = id; + this.title = title; + this.createdAt = createdAt.plusHours(9L); + this.updatedAt = updatedAt.plusHours(9L); + this.preview = preview; + this.recent = recent; } } diff --git a/src/main/java/io/ejangs/docsa/domain/doc/entity/Doc.java b/src/main/java/io/ejangs/docsa/domain/doc/entity/Doc.java index 35527fdd..2d48ecb1 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/entity/Doc.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/entity/Doc.java @@ -2,7 +2,6 @@ import io.ejangs.docsa.domain.branch.entity.Branch; import io.ejangs.docsa.domain.edge.entity.Edge; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; import io.ejangs.docsa.domain.user.entity.User; import io.ejangs.docsa.global.common.BaseEntity; import jakarta.persistence.CascadeType; @@ -15,7 +14,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import java.util.ArrayList; @@ -51,9 +49,6 @@ public class Doc extends BaseEntity { @OneToMany(mappedBy = "doc", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List edges; - @OneToOne(mappedBy = "doc", cascade = CascadeType.ALL, orphanRemoval = true) - private Thumbnail thumbnail; - @Builder private Doc(String title, User user) { this.title = title; diff --git a/src/main/java/io/ejangs/docsa/domain/doc/swagger/GetDocGraphDocs.java b/src/main/java/io/ejangs/docsa/domain/doc/swagger/GetDocGraphDocs.java index e7cca649..e146288e 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/swagger/GetDocGraphDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/swagger/GetDocGraphDocs.java @@ -160,11 +160,11 @@ value = """ { "status": 404, - "message": "해당 브랜치를 찾을 수 없습니다.", + "message": "해당 버전을 찾을 수 없습니다.", "error": "BRANCH_NOT_FOUND" } """, - name = "브랜치를 찾을 수 없음." + name = "버전을 찾을 수 없음." ), } diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/api/ThumbnailController.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/api/ThumbnailController.java deleted file mode 100644 index bcc773cb..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/api/ThumbnailController.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.api; - -import io.ejangs.docsa.domain.doc.thumbnail.app.ThumbnailService; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailFinalizeRequest; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailResponse; -import io.ejangs.docsa.domain.doc.thumbnail.swagger.FinalizeThumbnailDocs; -import io.ejangs.docsa.domain.user.security.CustomUserDetails; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/document/{docId}/thumbnail") -@Tag(name = "Thumbnail API", description = "문서 대표 썸네일 API") -public class ThumbnailController { - - private final ThumbnailService thumbnailService; - - @PutMapping - @FinalizeThumbnailDocs - public ResponseEntity finalizeThumbnail( - @AuthenticationPrincipal CustomUserDetails userDetails, - @PathVariable Long docId, - @Valid @RequestBody ThumbnailFinalizeRequest request - ) { - return ResponseEntity.ok(thumbnailService.finalizeThumbnail( - userDetails.getId(), - docId, - request.imageId(), - request.requestToken(), - request.signature() - )); - } - - -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailQueryService.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailQueryService.java deleted file mode 100644 index 25dca073..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailQueryService.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.app; - -import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.dao.ThumbnailRepository; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; -import io.ejangs.docsa.global.exception.CustomException; -import io.ejangs.docsa.global.exception.errorcode.ThumbnailErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ThumbnailQueryService { - - private final ThumbnailRepository thumbnailRepository; - - public Thumbnail getByDocIdForUpdate(Long docId) { - return thumbnailRepository.findByDocIdForUpdate(docId) - .orElseThrow(() -> new CustomException(ThumbnailErrorCode.THUMBNAIL_NOT_FOUND)); - } - - public Thumbnail getOrCreateByDocForUpdate(Doc doc) { - return thumbnailRepository.findByDocIdForUpdate(doc.getId()) - .orElseGet(() -> thumbnailRepository.save(Thumbnail.builder() - .doc(doc) - .build())); - } - -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailService.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailService.java deleted file mode 100644 index 15c79843..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailService.java +++ /dev/null @@ -1,99 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.app; - -import io.ejangs.docsa.domain.doc.app.create.DocQueryService; -import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailResponse; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailSyncResponse; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; -import io.ejangs.docsa.domain.image.app.ImageQueryService; -import io.ejangs.docsa.domain.image.entity.Image; -import io.ejangs.docsa.global.outbox.s3.app.S3DeleteOutboxFactory; -import io.ejangs.docsa.global.exception.CustomException; -import io.ejangs.docsa.global.exception.errorcode.ImageErrorCode; -import io.ejangs.docsa.global.exception.errorcode.ThumbnailErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class ThumbnailService { - - private final ThumbnailQueryService thumbnailQueryService; - private final DocQueryService docQueryService; - private final ImageQueryService imageQueryService; - private final S3DeleteOutboxFactory s3DeleteOutboxFactory; - - @Value("${cloud.aws.s3.public-base-url}") - private String cdnUrl; - - @Transactional - public ThumbnailSyncResponse requestUpdate(Long userId, Long docId) { - Doc doc = docQueryService.getByIdAndUserId(docId, userId); - - Thumbnail thumbnail = thumbnailQueryService.getOrCreateByDocForUpdate(doc); - - Long requestToken = thumbnail.requestUpdate(); - - return new ThumbnailSyncResponse( - requestToken, - thumbnail.getSignature(), - thumbnail.getStatus() - ); - } - - @Transactional - public ThumbnailResponse finalizeThumbnail( - Long userId, - Long docId, - Long imageId, - Long requestToken, - String signature - ) { - docQueryService.checkByIdAndUserId(docId, userId); - - Thumbnail thumbnail = thumbnailQueryService.getByDocIdForUpdate(docId); - - if (!thumbnail.isCurrentToken(requestToken)) { - throw new CustomException(ThumbnailErrorCode.STALE_THUMBNAIL_REQUEST); - } - - Image image = imageQueryService.getByIdAndUserId(imageId, userId); - - validateThumbnailImage(docId, image); - - Image previousImage = thumbnail.getCurrentImage(); - thumbnail.complete(image, signature); - enqueuePreviousThumbnailDeletion(previousImage, image); - - return new ThumbnailResponse( - image.getId(), - "%s/%s".formatted(cdnUrl, image.getObjectKey()), - thumbnail.getStatus(), - thumbnail.getSignature() - ); - } - - private void validateThumbnailImage(Long docId, Image image) { - if (!image.getDocId().equals(docId)) { - throw new CustomException(ThumbnailErrorCode.THUMBNAIL_NOT_FOUND); - } - - if (image.getStatus() != Image.ImageStatus.ACTIVE) { - throw new CustomException(ImageErrorCode.IMAGE_UPLOAD_NOT_COMPLETED); - } - - if (image.getPurpose() != Image.Purpose.DOC_THUMBNAIL) { - throw new CustomException(ThumbnailErrorCode.INVALID_THUMBNAIL_PURPOSE); - } - } - - private void enqueuePreviousThumbnailDeletion(Image previousImage, Image currentImage) { - if (previousImage == null || previousImage.getId().equals(currentImage.getId())) { - return; - } - - s3DeleteOutboxFactory.enqueueImageDeletion(previousImage); - } -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dao/ThumbnailRepository.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dao/ThumbnailRepository.java deleted file mode 100644 index a24865e3..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dao/ThumbnailRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.dao; - -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; -import jakarta.persistence.LockModeType; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface ThumbnailRepository extends JpaRepository { - - Optional findByDocId(Long docId); - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query(""" - select t - from Thumbnail t - where t.doc.id = :docId - """) - Optional findByDocIdForUpdate(@Param("docId") Long docId); - - - @Query(""" - select t - from Thumbnail t - left join fetch t.currentImage - where t.doc.id in :docIds - """) - List findAllByDocIdInWithCurrentImage(@Param("docIds") Collection docIds); -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailFinalizeRequest.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailFinalizeRequest.java deleted file mode 100644 index ad71dfe6..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailFinalizeRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "문서 대표 썸네일 확정 요청") -public record ThumbnailFinalizeRequest( - @Schema(description = "대표 썸네일로 확정할 이미지 id", example = "10") - @NotNull - Long imageId, - - @Schema(description = "저장 API 응답으로 받은 최신 썸네일 요청 토큰", example = "12") - @NotNull - Long requestToken, - - @Schema(description = "프론트가 썸네일 캡처 대상 영역 기준으로 계산한 signature", example = "9f0c1dd5d7e8b8f2a1f4c3d2e6a7b8c9") - @NotBlank - String signature -) { - -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailResponse.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailResponse.java deleted file mode 100644 index 7a69ff1f..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.dto; - -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "문서 대표 썸네일 응답") -public record ThumbnailResponse( - @Schema(description = "대표 썸네일 이미지 id", example = "10") - Long imageId, - @Schema(description = "대표 썸네일 CDN URL", example = "https://cdn.example.com/users/1/docs/1/images/550e8400-e29b-41d4-a716-446655440000.webp") - String thumbnailUrl, - @Schema(description = "썸네일 상태(EMPTY/PENDING/READY/FAILED)", example = "READY") - ThumbnailStatus status, - @Schema(description = "대표 썸네일이 반영한 프론트 상단 영역 signature", example = "9f0c1dd5d7e8b8f2a1f4c3d2e6a7b8c9") - String signature -) { - -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailSyncResponse.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailSyncResponse.java deleted file mode 100644 index 2e9cd44c..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/dto/ThumbnailSyncResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.dto; - -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "저장 후 썸네일 동기화 정보") -public record ThumbnailSyncResponse( - @Schema(description = "최신 썸네일 요청 토큰. finalize 시 그대로 전달해야 합니다.", example = "12") - Long requestToken, - @Schema(description = "현재 대표 썸네일이 반영한 signature. 프론트가 계산한 signature와 같으면 업로드를 생략합니다.", example = "9f0c1dd5d7e8b8f2a1f4c3d2e6a7b8c9") - String signature, - @Schema(description = "썸네일 상태(EMPTY/PENDING/READY/FAILED)", example = "PENDING") - ThumbnailStatus status -) { - -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/entity/Thumbnail.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/entity/Thumbnail.java deleted file mode 100644 index b8081ff3..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/entity/Thumbnail.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.entity; - -import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.image.entity.Image; -import io.ejangs.docsa.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import java.time.LocalDateTime; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import jakarta.persistence.Version; - -@Entity -@Getter -@Table(name = "doc_thumbnails") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Thumbnail extends BaseEntity { - - public enum ThumbnailStatus { - EMPTY, - PENDING, - READY, - FAILED - } - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "doc_id", nullable = false, unique = true) - private Doc doc; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "current_image_id", unique = true) - private Image currentImage; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private ThumbnailStatus status; - - @Column(nullable = false) - private Long requestToken; - - @Column(length = 255) - private String signature; - - private LocalDateTime requestedAt; - - private LocalDateTime generatedAt; - - @Column(length = 500) - private String lastError; - - @Version - private Long version; - - @Builder - private Thumbnail(Doc doc) { - this.doc = doc; - this.status = ThumbnailStatus.EMPTY; - this.requestToken = 0L; - } - - public Long requestUpdate() { - this.requestToken++; - if (this.currentImage == null) { - this.status = ThumbnailStatus.PENDING; - } - this.requestedAt = LocalDateTime.now(); - this.lastError = null; - - return this.requestToken; - } - - public boolean isCurrentToken(Long requestToken) { - return this.requestToken.equals(requestToken); - } - - public void complete(Image image, String signature) { - this.currentImage = image; - this.signature = signature; - this.status = ThumbnailStatus.READY; - this.generatedAt = LocalDateTime.now(); - this.lastError = null; - } - - public void fail(String lastError) { - this.status = ThumbnailStatus.FAILED; - this.lastError = lastError; - } -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/swagger/FinalizeThumbnailDocs.java b/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/swagger/FinalizeThumbnailDocs.java deleted file mode 100644 index e2b2363c..00000000 --- a/src/main/java/io/ejangs/docsa/domain/doc/thumbnail/swagger/FinalizeThumbnailDocs.java +++ /dev/null @@ -1,173 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.swagger; - -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailFinalizeRequest; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailResponse; -import io.ejangs.docsa.global.exception.ErrorResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.springframework.http.MediaType; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Operation( - summary = "문서 대표 썸네일 확정", - description = """ - 프론트가 생성한 썸네일 이미지를 문서의 대표 썸네일로 확정합니다. - 저장 API 응답의 `thumbnail.requestToken`과 프론트가 계산한 `signature`를 함께 전송해야 합니다. - 서버는 requestToken을 검증해 오래된 업로드 결과가 최신 썸네일을 덮어쓰지 못하게 막습니다. - 확정하려는 이미지는 같은 문서에 연결된 `DOC_THUMBNAIL` 목적의 `ACTIVE` 이미지여야 합니다. - 🔐 이 API는 세션 로그인 상태에서 호출되어야 하며, - 클라이언트는 쿠키(`JSESSIONID`)를 통해 인증 정보를 전송해야 합니다. - """, - parameters = { - @Parameter( - name = "docId", - description = "썸네일을 확정할 문서 id", - example = "1", - required = true, - in = ParameterIn.PATH - ) - }, - requestBody = @RequestBody( - required = true, - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ThumbnailFinalizeRequest.class), - examples = @ExampleObject( - value = """ - { - "imageId": 10, - "requestToken": 12, - "signature": "9f0c1dd5d7e8b8f2a1f4c3d2e6a7b8c9" - } - """ - ) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "썸네일 확정 성공", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ThumbnailResponse.class), - examples = @ExampleObject( - value = """ - { - "imageId": 10, - "thumbnailUrl": "https://cdn.example.com/users/1/docs/1/images/550e8400-e29b-41d4-a716-446655440000.webp", - "status": "READY", - "signature": "9f0c1dd5d7e8b8f2a1f4c3d2e6a7b8c9" - } - """ - ) - ) - ), - @ApiResponse( - responseCode = "400", - description = "썸네일 용도가 아닌 이미지", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ErrorResponse.class), - examples = @ExampleObject( - value = """ - { - "status": 400, - "message": "썸네일 용도의 이미지가 아닙니다.", - "error": "INVALID_THUMBNAIL_PURPOSE" - } - """ - ) - ) - ), - @ApiResponse( - responseCode = "401", - description = "인증 실패 - 로그인 세션이 없음", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ErrorResponse.class), - examples = @ExampleObject( - value = """ - { - "status": 401, - "message": "로그인이 필요합니다.", - "error": "LOGIN_REQUIRED" - } - """ - ) - ) - ), - @ApiResponse( - responseCode = "404", - description = "문서, 이미지 또는 썸네일 정보를 찾을 수 없음", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "썸네일 정보 없음", - value = """ - { - "status": 404, - "message": "썸네일 정보를 찾을 수 없습니다.", - "error": "THUMBNAIL_NOT_FOUND" - } - """ - ), - @ExampleObject( - name = "이미지 없음", - value = """ - { - "status": 404, - "message": "이미지를 찾을 수 없습니다.", - "error": "IMAGE_NOT_FOUND" - } - """ - ) - } - ) - ), - @ApiResponse( - responseCode = "409", - description = "오래된 requestToken 또는 S3 업로드 미완료", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "오래된 썸네일 요청", - value = """ - { - "status": 409, - "message": "최신 썸네일 요청이 아닙니다.", - "error": "STALE_THUMBNAIL_REQUEST" - } - """ - ), - @ExampleObject( - name = "이미지 업로드 미완료", - value = """ - { - "status": 409, - "message": "이미지 업로드가 아직 완료되지 않았습니다.", - "error": "IMAGE_UPLOAD_NOT_COMPLETED" - } - """ - ) - } - ) - ) - } -) -public @interface FinalizeThumbnailDocs { -} diff --git a/src/main/java/io/ejangs/docsa/domain/doc/util/DocListAssembler.java b/src/main/java/io/ejangs/docsa/domain/doc/util/DocListAssembler.java index c201da98..87ab479f 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/util/DocListAssembler.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/util/DocListAssembler.java @@ -1,19 +1,21 @@ package io.ejangs.docsa.domain.doc.util; import io.ejangs.docsa.domain.branch.entity.Branch; +import io.ejangs.docsa.domain.commit.app.CommitContentAssembler; +import io.ejangs.docsa.domain.commit.entity.Commit; import io.ejangs.docsa.domain.doc.dto.RecentActivityDto; import io.ejangs.docsa.domain.doc.dto.response.DocPageResponse; import io.ejangs.docsa.domain.doc.dto.response.DocSimplePageResponse; import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.dao.ThumbnailRepository; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; +import io.ejangs.docsa.domain.save.dao.mongodb.SaveContentRepository; +import io.ejangs.docsa.domain.save.document.SaveContent; +import io.ejangs.docsa.domain.save.entity.Save; +import io.ejangs.docsa.global.exception.CustomException; +import io.ejangs.docsa.global.exception.errorcode.SaveErrorCode; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; @@ -21,63 +23,65 @@ @RequiredArgsConstructor public class DocListAssembler { - private final ThumbnailRepository thumbnailRepository; + private final CommitContentAssembler commitContentAssembler; + private final SaveContentRepository saveContentRepository; - @Value("${cloud.aws.s3.public-base-url}") - private String cdnUrl; + private static final String DEFAULT_PREVIEW = "미리보기 없음"; public Page assembleDocList(Page docs) { - List docIds = docs.getContent().stream() - .map(Doc::getId) - .toList(); - - Map thumbnailByDocId = thumbnailRepository - .findAllByDocIdInWithCurrentImage(docIds) - .stream() - .collect(Collectors.toMap( - thumbnail -> thumbnail.getDoc().getId(), - Function.identity() - )); - - return docs.map(doc -> { - Branch recentBranch = getMostRecentBranch(doc); - RecentActivityDto recent = getRecentActivity(recentBranch); - Thumbnail thumbnail = thumbnailByDocId.get(doc.getId()); - - return DocMapper.toListResponse( - doc, - buildThumbnailUrl(thumbnail), - thumbnailStatusOf(thumbnail), - recent - ); - }); + return docs + .map(doc -> { + Branch recentBranch = getMostRecentBranch(doc); + RecentActivityDto recent = getRecentActivity(recentBranch); + String preview = extractPreviewSafe(recentBranch, recent); + return DocMapper.toListResponse(doc, preview, recent); + }); } - private String buildThumbnailUrl(Thumbnail thumbnail) { - if (thumbnail == null || thumbnail.getCurrentImage() == null) { - return null; + public Page assembleDocListSimple(Page docs) { + return docs + .map(doc -> { + Branch recentBranch = getMostRecentBranch(doc); + RecentActivityDto recent = getRecentActivity(recentBranch); + return DocMapper.toListSimpleResponse(doc, recent); + }); + } + + private String extractPreviewSafe(Branch branch, RecentActivityDto recent) { + if (branch == null || recent == null) { + return DEFAULT_PREVIEW; } - return "%s/%s".formatted(cdnUrl, thumbnail.getCurrentImage().getObjectKey()); + return switch (recent.recentType()) { + case COMMIT -> extractPreviewFromCommit(branch.getLeafCommit()); + case SAVE -> extractPreviewFromSave(branch.getSave()); + default -> DEFAULT_PREVIEW; + }; } - private Thumbnail.ThumbnailStatus thumbnailStatusOf(Thumbnail thumbnail) { - if (thumbnail == null) { - return Thumbnail.ThumbnailStatus.EMPTY; + private String extractPreviewFromCommit(Commit commit) { + if (commit == null) { + return DEFAULT_PREVIEW; } - return thumbnail.getStatus(); + + List> content = commitContentAssembler.assemble( + commit.getCommitMongoId()); + return PreviewExtractor.doExtractPreview(content); } + private String extractPreviewFromSave(Save save) { + if (save == null) { + return DEFAULT_PREVIEW; + } - public Page assembleDocListSimple(Page docs) { - return docs - .map(doc -> { - Branch recentBranch = getMostRecentBranch(doc); - RecentActivityDto recent = getRecentActivity(recentBranch); - return DocMapper.toListSimpleResponse(doc, recent); - }); + SaveContent saveContent = saveContentRepository.findById(save.getSaveMongoId()) + .orElseThrow(() -> new CustomException(SaveErrorCode.SAVE_NOT_FOUND)); + + List> content = saveContent.getContent(); + return PreviewExtractor.doExtractPreview(content); } + private Branch getMostRecentBranch(Doc doc) { return doc.getBranches().stream() .max(Comparator.comparing(Branch::getUpdatedAt)) diff --git a/src/main/java/io/ejangs/docsa/domain/doc/util/DocMapper.java b/src/main/java/io/ejangs/docsa/domain/doc/util/DocMapper.java index 8a9b0d83..a08151d4 100644 --- a/src/main/java/io/ejangs/docsa/domain/doc/util/DocMapper.java +++ b/src/main/java/io/ejangs/docsa/domain/doc/util/DocMapper.java @@ -6,7 +6,6 @@ import io.ejangs.docsa.domain.doc.dto.response.DocSimplePageResponse; import io.ejangs.docsa.domain.doc.dto.response.DocTitleUpdateResponse; import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; import io.ejangs.docsa.domain.save.entity.Save; public class DocMapper { @@ -33,16 +32,14 @@ public static DocSimplePageResponse toListSimpleResponse(Doc doc, RecentActivity ); } - public static DocPageResponse toListResponse(Doc doc, String thumbnailUrl, - ThumbnailStatus thumbnailStatus, + public static DocPageResponse toListResponse(Doc doc, String preview, RecentActivityDto recent) { return new DocPageResponse( doc.getId(), doc.getTitle(), doc.getCreatedAt(), doc.getUpdatedAt(), - thumbnailUrl, - thumbnailStatus, + preview, recent ); } diff --git a/src/main/java/io/ejangs/docsa/domain/image/app/ImageQueryService.java b/src/main/java/io/ejangs/docsa/domain/image/app/ImageQueryService.java deleted file mode 100644 index f93eee2b..00000000 --- a/src/main/java/io/ejangs/docsa/domain/image/app/ImageQueryService.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.ejangs.docsa.domain.image.app; - -import io.ejangs.docsa.domain.image.dao.ImageRepository; -import io.ejangs.docsa.domain.image.entity.Image; -import io.ejangs.docsa.global.exception.CustomException; -import io.ejangs.docsa.global.exception.errorcode.ImageErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ImageQueryService { - - private final ImageRepository imageRepository; - - public Image getByIdAndUserId(Long imageId, Long userId) { - return imageRepository.findByIdAndUserId(imageId, userId) - .orElseThrow(() -> new CustomException(ImageErrorCode.IMAGE_NOT_FOUND)); - } - -} diff --git a/src/main/java/io/ejangs/docsa/domain/image/app/ImageService.java b/src/main/java/io/ejangs/docsa/domain/image/app/ImageService.java index 44030fc8..0e91ea9f 100644 --- a/src/main/java/io/ejangs/docsa/domain/image/app/ImageService.java +++ b/src/main/java/io/ejangs/docsa/domain/image/app/ImageService.java @@ -6,7 +6,6 @@ import io.ejangs.docsa.domain.image.dto.response.ImageUploadCompleteResponse; import io.ejangs.docsa.domain.image.dto.response.ImageUploadUrlResponse; import io.ejangs.docsa.domain.image.entity.Image; -import io.ejangs.docsa.domain.image.entity.Image.Purpose; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.ImageErrorCode; import java.time.Duration; @@ -32,7 +31,6 @@ public class ImageService { private static final long MAX_IMAGE_SIZE = 5 * 1024 * 1024; private final ImageRepository imageRepository; - private final ImageQueryService imageQueryService; private final DocQueryService docQueryService; private final S3Presigner s3Presigner; private final S3Client s3Client; @@ -56,8 +54,7 @@ public ImageUploadUrlResponse createUploadUrl(Long userId, ImageUploadUrlRequest validateImage(request.contentType(), request.size()); String extension = extensionOf(request.contentType()); - - String objectKey = objectKeyOf(request.purpose()) + String objectKey = "users/%d/docs/%d/images/%s.%s" .formatted(userId, request.docId(), UUID.randomUUID(), extension); Image image = imageRepository.save(Image.builder() @@ -67,7 +64,6 @@ public ImageUploadUrlResponse createUploadUrl(Long userId, ImageUploadUrlRequest .objectKey(objectKey) .contentType(request.contentType()) .size(request.size()) - .purpose(request.purpose()) .build()); PutObjectRequest putObjectRequest = PutObjectRequest.builder() @@ -92,11 +88,14 @@ public ImageUploadUrlResponse createUploadUrl(Long userId, ImageUploadUrlRequest "PUT", expireMinutes * 60 ); + + } @Transactional public ImageUploadCompleteResponse complete(Long userId, Long imageId) { - Image image = imageQueryService.getByIdAndUserId(imageId, userId); + Image image = imageRepository.findByIdAndUserId(imageId, userId) + .orElseThrow(() -> new CustomException(ImageErrorCode.IMAGE_NOT_FOUND)); String objectKey = image.getObjectKey(); @@ -139,13 +138,6 @@ private String extensionOf(String contentType) { }; } - private String objectKeyOf(Purpose purpose) { - return switch (purpose) { - case DOC_CONTENT -> "users/%d/docs/%d/images/%s.%s"; - case DOC_THUMBNAIL -> "users/%d/docs/%d/thumbnails/%s.%s"; - }; - } - private HeadObjectResponse getHeadObjectOrThrow(String objectKey) { try { return s3Client.headObject(HeadObjectRequest.builder() diff --git a/src/main/java/io/ejangs/docsa/domain/image/dto/request/ImageUploadUrlRequest.java b/src/main/java/io/ejangs/docsa/domain/image/dto/request/ImageUploadUrlRequest.java index bdb630a0..91aa28b3 100644 --- a/src/main/java/io/ejangs/docsa/domain/image/dto/request/ImageUploadUrlRequest.java +++ b/src/main/java/io/ejangs/docsa/domain/image/dto/request/ImageUploadUrlRequest.java @@ -1,6 +1,5 @@ package io.ejangs.docsa.domain.image.dto.request; -import io.ejangs.docsa.domain.image.entity.Image.Purpose; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -23,11 +22,7 @@ public record ImageUploadUrlRequest( @Schema(description = "파일 크기(byte)", example = "102400") @NotNull(message = "Size Cannot be Null") @Positive(message = "Size Must be Positive Number") - Long size, - - @Schema(description = "이미지 업로드 목적(DOC_CONTENT/DOC_THUMBNAIL)", example = "DOC_CONTENT") - @NotNull(message = "Purpose Cannot be Null") - Purpose purpose + Long size ) { } diff --git a/src/main/java/io/ejangs/docsa/domain/image/entity/Image.java b/src/main/java/io/ejangs/docsa/domain/image/entity/Image.java index 1a2294ca..027184c0 100644 --- a/src/main/java/io/ejangs/docsa/domain/image/entity/Image.java +++ b/src/main/java/io/ejangs/docsa/domain/image/entity/Image.java @@ -23,16 +23,9 @@ public class Image extends BaseEntity { public enum ImageStatus { PENDING, ACTIVE, - DELETING, - DELETED, FAILED } - public enum Purpose { - DOC_CONTENT, - DOC_THUMBNAIL - } - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -59,19 +52,14 @@ public enum Purpose { @Column(nullable = false) private ImageStatus status; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Purpose purpose; - @Builder - private Image(Long userId, Long docId, String originalFileName, String objectKey, String contentType, Long size, Purpose purpose) { + private Image(Long userId, Long docId, String originalFileName, String objectKey, String contentType, Long size) { this.userId = userId; this.docId = docId; this.originalFileName = originalFileName; this.objectKey = objectKey; this.contentType = contentType; this.size = size; - this.purpose = purpose; this.status = ImageStatus.PENDING; } @@ -79,14 +67,6 @@ public void activate() { this.status = ImageStatus.ACTIVE; } - public void markDeleting() { - this.status = ImageStatus.DELETING; - } - - public void markDeleted() { - this.status = ImageStatus.DELETED; - } - public void fail() { this.status = ImageStatus.FAILED; } diff --git a/src/main/java/io/ejangs/docsa/domain/image/swagger/CreateImageUploadUrlDocs.java b/src/main/java/io/ejangs/docsa/domain/image/swagger/CreateImageUploadUrlDocs.java index d1eeb598..48173218 100644 --- a/src/main/java/io/ejangs/docsa/domain/image/swagger/CreateImageUploadUrlDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/image/swagger/CreateImageUploadUrlDocs.java @@ -37,8 +37,7 @@ "docId": 1, "originalFileName": "profile.png", "contentType": "image/png", - "size": 102400, - "purpose": "DOC_CONTENT" + "size": 102400 } """ ) diff --git a/src/main/java/io/ejangs/docsa/domain/save/app/SaveService.java b/src/main/java/io/ejangs/docsa/domain/save/app/SaveService.java index c47a0afe..64e109ab 100644 --- a/src/main/java/io/ejangs/docsa/domain/save/app/SaveService.java +++ b/src/main/java/io/ejangs/docsa/domain/save/app/SaveService.java @@ -1,8 +1,6 @@ package io.ejangs.docsa.domain.save.app; import com.mongodb.DuplicateKeyException; -import io.ejangs.docsa.domain.doc.thumbnail.app.ThumbnailService; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailSyncResponse; import io.ejangs.docsa.domain.save.document.SaveContent; import io.ejangs.docsa.domain.save.dto.SaveIdentifierDto; import io.ejangs.docsa.domain.save.dto.request.SaveUpdateRequest; @@ -26,7 +24,6 @@ public class SaveService { private final SaveQueryService saveQueryService; - private final ThumbnailService thumbnailService; @Transactional(readOnly = true) public SaveGetResponse getSave(SaveIdentifierDto dto) { @@ -44,11 +41,6 @@ public SaveUpdateResponse updateSave(SaveIdentifierDto dto, SaveUpdateRequest re RenewUpdatedAtHelper.touch(findSave); saveQueryService.saveSave(findSave); - ThumbnailSyncResponse thumbnailSyncResponse = thumbnailService.requestUpdate( - dto.userId(), - dto.documentId() - ); - // MongoDB 저장 try { SaveContent saveContent = saveQueryService.getSaveContentById(findSave.getSaveMongoId()); @@ -63,13 +55,14 @@ public SaveUpdateResponse updateSave(SaveIdentifierDto dto, SaveUpdateRequest re throw new CustomException(SaveErrorCode.FAIL_TO_SAVE); } - return SaveMapper.toSaveUpdateResponse(findSave.getUpdatedAt(), thumbnailSyncResponse); + return SaveMapper.toSaveUpdateResponse(findSave.getUpdatedAt()); } private Save getValidSave(SaveIdentifierDto dto) { - + // 존재하는 user, document 인지 검사 Save findSave = saveQueryService.getSaveById(dto.saveId()); + // SAVE 소유자인지 검사 saveQueryService.checkSaveAndDocOwner(findSave, dto.userId(), dto.documentId()); return findSave; } diff --git a/src/main/java/io/ejangs/docsa/domain/save/dto/response/SaveUpdateResponse.java b/src/main/java/io/ejangs/docsa/domain/save/dto/response/SaveUpdateResponse.java index b1c34d32..b17be346 100644 --- a/src/main/java/io/ejangs/docsa/domain/save/dto/response/SaveUpdateResponse.java +++ b/src/main/java/io/ejangs/docsa/domain/save/dto/response/SaveUpdateResponse.java @@ -1,19 +1,10 @@ package io.ejangs.docsa.domain.save.dto.response; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailSyncResponse; -import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; -@Schema(description = "저장 수정 응답") -public record SaveUpdateResponse( - @Schema(description = "저장 수정 시각", example = "2026-04-26T21:30:00") - LocalDateTime updatedAt, - @Schema(description = "프론트 썸네일 동기화 판단에 필요한 정보") - ThumbnailSyncResponse thumbnail -) { +public record SaveUpdateResponse(LocalDateTime updatedAt) { - public SaveUpdateResponse(LocalDateTime updatedAt, ThumbnailSyncResponse thumbnail) { + public SaveUpdateResponse(LocalDateTime updatedAt) { this.updatedAt = updatedAt.plusHours(9L); - this.thumbnail = thumbnail; } } diff --git a/src/main/java/io/ejangs/docsa/domain/save/swagger/UpdateSaveDocs.java b/src/main/java/io/ejangs/docsa/domain/save/swagger/UpdateSaveDocs.java index 6815717e..60fdd294 100644 --- a/src/main/java/io/ejangs/docsa/domain/save/swagger/UpdateSaveDocs.java +++ b/src/main/java/io/ejangs/docsa/domain/save/swagger/UpdateSaveDocs.java @@ -94,12 +94,7 @@ examples = @ExampleObject( value = """ { - "updatedAt": "2026-04-26T21:30:00", - "thumbnail": { - "requestToken": 12, - "signature": "9f0c1dd5d7e8b8f2a1f4c3d2e6a7b8c9", - "status": "PENDING" - } + "updatedAt": "2025-07-07T14:21:00" } """ ) diff --git a/src/main/java/io/ejangs/docsa/domain/save/util/SaveMapper.java b/src/main/java/io/ejangs/docsa/domain/save/util/SaveMapper.java index 3bf5c9c1..94113f7b 100644 --- a/src/main/java/io/ejangs/docsa/domain/save/util/SaveMapper.java +++ b/src/main/java/io/ejangs/docsa/domain/save/util/SaveMapper.java @@ -1,6 +1,5 @@ package io.ejangs.docsa.domain.save.util; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailSyncResponse; import io.ejangs.docsa.domain.save.dto.response.SaveGetResponse; import io.ejangs.docsa.domain.save.dto.response.SaveUpdateResponse; import java.time.LocalDateTime; @@ -9,9 +8,8 @@ public class SaveMapper { - public static SaveUpdateResponse toSaveUpdateResponse(LocalDateTime localDateTime, - ThumbnailSyncResponse thumbnailSyncResponse) { - return new SaveUpdateResponse(localDateTime, thumbnailSyncResponse); + public static SaveUpdateResponse toSaveUpdateResponse(LocalDateTime localDateTime) { + return new SaveUpdateResponse(localDateTime); } public static SaveGetResponse toSaveGetResponse(LocalDateTime localDateTime, diff --git a/src/main/java/io/ejangs/docsa/global/config/JpaConfig.java b/src/main/java/io/ejangs/docsa/global/config/JpaConfig.java index 3b7e2b36..ccd8f25d 100644 --- a/src/main/java/io/ejangs/docsa/global/config/JpaConfig.java +++ b/src/main/java/io/ejangs/docsa/global/config/JpaConfig.java @@ -15,13 +15,11 @@ "io.ejangs.docsa.domain.branch.dao.mysql", "io.ejangs.docsa.domain.commit.dao.mysql", "io.ejangs.docsa.domain.doc.dao.mysql", - "io.ejangs.docsa.domain.doc.thumbnail.dao", "io.ejangs.docsa.domain.edge.dao.mysql", "io.ejangs.docsa.domain.save.dao.mysql", "io.ejangs.docsa.domain.user.dao.mysql", - "io.ejangs.docsa.global.outbox.mongo.dao.mysql", - "io.ejangs.docsa.domain.image.dao", - "io.ejangs.docsa.global.outbox.s3.dao" + "io.ejangs.docsa.global.mongo.outbox.dao.mysql", + "io.ejangs.docsa.domain.image.dao" }) public class JpaConfig { diff --git a/src/main/java/io/ejangs/docsa/global/exception/errorcode/BranchErrorCode.java b/src/main/java/io/ejangs/docsa/global/exception/errorcode/BranchErrorCode.java index 1436f186..70d810cb 100644 --- a/src/main/java/io/ejangs/docsa/global/exception/errorcode/BranchErrorCode.java +++ b/src/main/java/io/ejangs/docsa/global/exception/errorcode/BranchErrorCode.java @@ -8,13 +8,14 @@ @RequiredArgsConstructor public enum BranchErrorCode implements ErrorCode { - BRANCH_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 브랜치를 찾을 수 없습니다.", "BRANCH_NOT_FOUND"), - MAIN_BRANCH_FIX_UNAVAILABLE(HttpStatus.BAD_REQUEST, "메인 브랜치는 삭제 또는 수정할 수 없습니다.", "MAIN_BRANCH_FIX_UNAVAILABLE"), - SUB_BRANCH_DELETE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "서브 브랜치가 있어 해당 브랜치를 삭제할 수 없습니다.", "SUB_BRANCH_DELETE_UNAVAILABLE"), - BRANCH_DELETE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "해당 브랜치를 삭제할 수 없습니다.", "BRANCH_DELETE_UNAVAILABLE"), - BRANCH_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "새로운 브랜치의 이름은 다른 브랜치의 이름과 중복될 수 없습니다.", + BRANCH_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 분기를 찾을 수 없습니다.", "BRANCH_NOT_FOUND"), + BRANCH_NOT_FOUND_OR_FORBIDDEN(HttpStatus.NOT_FOUND, "해당 분기를 찾을 수 없습니다.", "BRANCH_NOT_FOUND_OR_FORBIDDEN"), + MAIN_BRANCH_FIX_UNAVAILABLE(HttpStatus.BAD_REQUEST, "Main 분기는 삭제 또는 수정할 수 없습니다.", "MAIN_BRANCH_FIX_UNAVAILABLE"), + SUB_BRANCH_DELETE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "서브 분기가 있어 해당 버전을 삭제할 수 없습니다.", "SUB_BRANCH_DELETE_UNAVAILABLE"), + BRANCH_DELETE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "해당 분기를 삭제할 수 없습니다.", "BRANCH_DELETE_UNAVAILABLE"), + BRANCH_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "새로운 분기의 이름은 다른 버전의 이름과 중복될 수 없습니다.", "BRANCH_NAME_DUPLICATED"), - FAIL_CREATE_BRANCH(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류로 인해 브랜치 생성에 실패했습니다.", + FAIL_CREATE_BRANCH(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류로 인해 분기 생성에 실패했습니다.", "FAIL_CREATE_BRANCH"); private final HttpStatus status; private final String message; diff --git a/src/main/java/io/ejangs/docsa/global/exception/errorcode/CommitErrorCode.java b/src/main/java/io/ejangs/docsa/global/exception/errorcode/CommitErrorCode.java index 86a75313..4f78fc7d 100644 --- a/src/main/java/io/ejangs/docsa/global/exception/errorcode/CommitErrorCode.java +++ b/src/main/java/io/ejangs/docsa/global/exception/errorcode/CommitErrorCode.java @@ -11,7 +11,7 @@ public enum CommitErrorCode implements ErrorCode { COMMIT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 기록을 찾을 수 없습니다.", "COMMIT_NOT_FOUND"), COMMIT_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.", "COMMIT_BAD_REQUEST"), CAN_NOT_DELETE_COMMIT(HttpStatus.BAD_REQUEST, "이 기록은 삭제할 수 없습니다.", "CAN_NOT_DELETE_COMMIT"), - IS_NOT_LEAF_COMMIT(HttpStatus.BAD_REQUEST, "브랜치의 마지막 기록이 아닙니다.", "IS_NOT_LEAF_COMMIT"), + IS_NOT_LEAF_COMMIT(HttpStatus.BAD_REQUEST, "버전의 마지막 기록이 아닙니다.", "IS_NOT_LEAF_COMMIT"), FAIL_CREATE_COMMIT(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류로 인해 기록 생성에 실패했습니다.", "FAIL_CREATE_COMMIT"), FAIL_MERGE(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류로 인해 병합에 실패했습니다.", "FAIL_MERGE"), @@ -23,3 +23,4 @@ public enum CommitErrorCode implements ErrorCode { private final String error; } + diff --git a/src/main/java/io/ejangs/docsa/global/exception/errorcode/ThumbnailErrorCode.java b/src/main/java/io/ejangs/docsa/global/exception/errorcode/ThumbnailErrorCode.java deleted file mode 100644 index 1d785fc3..00000000 --- a/src/main/java/io/ejangs/docsa/global/exception/errorcode/ThumbnailErrorCode.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.ejangs.docsa.global.exception.errorcode; - -import javax.swing.text.html.HTML; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@Getter -@RequiredArgsConstructor -public enum ThumbnailErrorCode implements ErrorCode{ - - THUMBNAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "썸네일 정보를 찾을 수 없습니다.", "THUMBNAIL_NOT_FOUND"), - STALE_THUMBNAIL_REQUEST(HttpStatus.CONFLICT, "최신 썸네일 요청이 아닙니다.", "STALE_THUMBNAIL_REQUEST"), - INVALID_THUMBNAIL_PURPOSE(HttpStatus.BAD_REQUEST, "썸네일 용도의 이미지가 아닙니다.", "INVALID_THUMBNAIL_PURPOSE"); - - private final HttpStatus status; - private final String message; - private final String error; -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxCreateService.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxCreateService.java similarity index 72% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxCreateService.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxCreateService.java index a20e7de3..45debab2 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxCreateService.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxCreateService.java @@ -1,7 +1,7 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxFactory.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxFactory.java similarity index 88% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxFactory.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxFactory.java index 90a7e42c..7d777820 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxFactory.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxFactory.java @@ -1,13 +1,13 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.DatabaseErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxLifecycleService.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxLifecycleService.java similarity index 71% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxLifecycleService.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxLifecycleService.java index 27aed7f8..c92104b9 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxLifecycleService.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxLifecycleService.java @@ -1,9 +1,8 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -20,18 +19,15 @@ public class MongoDeleteOutboxLifecycleService { private final MongoDeleteOutboxRepository mongoDeleteOutboxRepository; public MongoIdsDto claimOpen(Long outboxId) { - int claimed = mongoDeleteOutboxRepository.claimOpenById(outboxId); - if (claimed == 0) { - return null; - } - MongoDeleteOutbox targetOutbox = mongoDeleteOutboxRepository - .findByIdAndStatus(outboxId, OutboxStatus.PROCESSING) + .findByIdAndStatus(outboxId, MongoDeleteOutbox.OutboxStatus.OPEN) .orElse(null); if (targetOutbox == null) { return null; } + targetOutbox.markProcessing(); + mongoDeleteOutboxRepository.save(targetOutbox); return new MongoIdsDto( targetOutbox.getSaveContentIds(), targetOutbox.getCommitBlockSequenceIds(), @@ -41,7 +37,7 @@ public MongoIdsDto claimOpen(Long outboxId) { public void done(Long outboxId) { MongoDeleteOutbox targetOutbox = mongoDeleteOutboxRepository - .findByIdAndStatus(outboxId, OutboxStatus.PROCESSING) + .findByIdAndStatus(outboxId, MongoDeleteOutbox.OutboxStatus.PROCESSING) .orElse(null); if (targetOutbox == null) { return; @@ -53,7 +49,7 @@ public void done(Long outboxId) { public void retry(Long outboxId, String errorMessage) { MongoDeleteOutbox targetOutbox = mongoDeleteOutboxRepository - .findByIdAndStatus(outboxId, OutboxStatus.PROCESSING) + .findByIdAndStatus(outboxId, MongoDeleteOutbox.OutboxStatus.PROCESSING) .orElse(null); if (targetOutbox == null) { return; @@ -66,14 +62,14 @@ public void retry(Long outboxId, String errorMessage) { public int recoverTimedOutProcessing(LocalDateTime threshold) { List stuckOutboxes = mongoDeleteOutboxRepository .findTop100ByStatusAndUpdatedAtBeforeOrderByUpdatedAtAsc( - OutboxStatus.PROCESSING, + MongoDeleteOutbox.OutboxStatus.PROCESSING, threshold ); if (stuckOutboxes.isEmpty()) { return 0; } - stuckOutboxes.forEach(outbox -> outbox.recoverProcessingTimeout("PROCESSING timeout recovered")); + stuckOutboxes.forEach(outbox -> outbox.markRetry(outbox.getLastError() + " (recovered)")); mongoDeleteOutboxRepository.saveAll(stuckOutboxes); return stuckOutboxes.size(); } diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxWorker.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxWorker.java similarity index 86% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxWorker.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxWorker.java index e07eadb5..c2e0c2ef 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxWorker.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxWorker.java @@ -1,9 +1,8 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -43,7 +42,7 @@ public void run() { } List outboxes = mongoDeleteOutboxRepository - .findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus.OPEN); + .findTop100ByStatusOrderByCreatedAtAsc(MongoDeleteOutbox.OutboxStatus.OPEN); if (outboxes.isEmpty()) { return; } diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteService.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteService.java similarity index 90% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteService.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteService.java index fe401b31..0ec340ef 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteService.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteService.java @@ -1,9 +1,9 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; import io.ejangs.docsa.domain.block.dao.mongodb.BlockRepository; import io.ejangs.docsa.domain.commit.dao.mongodb.CommitBlockSequenceRepository; import io.ejangs.docsa.domain.save.dao.mongodb.SaveContentRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/io/ejangs/docsa/global/mongo/outbox/dao/mysql/MongoDeleteOutboxRepository.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/dao/mysql/MongoDeleteOutboxRepository.java new file mode 100644 index 00000000..49731edd --- /dev/null +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/dao/mysql/MongoDeleteOutboxRepository.java @@ -0,0 +1,29 @@ +package io.ejangs.docsa.global.mongo.outbox.dao.mysql; + +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MongoDeleteOutboxRepository extends JpaRepository { + + List findTop100ByStatusOrderByCreatedAtAsc(MongoDeleteOutbox.OutboxStatus status); + + List findTop100ByStatusAndUpdatedAtBeforeOrderByUpdatedAtAsc( + MongoDeleteOutbox.OutboxStatus status, + LocalDateTime updatedAt + ); + + Optional findByIdAndStatus(Long id, MongoDeleteOutbox.OutboxStatus status); + + Optional findByTriggerTypeAndDomainTypeAndOriginTypeAndOriginId( + TriggerType triggerType, + DomainType domainType, + OriginType originType, + String originId + ); +} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/dto/CommitMongoIdsDto.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/dto/CommitMongoIdsDto.java similarity index 70% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/dto/CommitMongoIdsDto.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/dto/CommitMongoIdsDto.java index 4e159a51..d0ef7d31 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/dto/CommitMongoIdsDto.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/dto/CommitMongoIdsDto.java @@ -1,4 +1,4 @@ -package io.ejangs.docsa.global.outbox.mongo.dto; +package io.ejangs.docsa.global.mongo.outbox.dto; import java.util.List; diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/dto/MongoIdsDto.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/dto/MongoIdsDto.java similarity index 94% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/dto/MongoIdsDto.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/dto/MongoIdsDto.java index e309bea9..0d8d13c2 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/dto/MongoIdsDto.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/dto/MongoIdsDto.java @@ -1,4 +1,4 @@ -package io.ejangs.docsa.global.outbox.mongo.dto; +package io.ejangs.docsa.global.mongo.outbox.dto; import java.util.List; diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/entity/MongoDeleteOutbox.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/entity/MongoDeleteOutbox.java similarity index 68% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/entity/MongoDeleteOutbox.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/entity/MongoDeleteOutbox.java index fad8a65f..ab92566d 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/entity/MongoDeleteOutbox.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/entity/MongoDeleteOutbox.java @@ -1,6 +1,6 @@ -package io.ejangs.docsa.global.outbox.mongo.entity; +package io.ejangs.docsa.global.mongo.outbox.entity; -import io.ejangs.docsa.global.outbox.BaseOutboxEntity; +import io.ejangs.docsa.global.common.BaseEntity; import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; @@ -10,10 +10,11 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import jakarta.persistence.Version; +import java.time.LocalDateTime; import java.util.List; import lombok.AccessLevel; import lombok.Getter; @@ -27,15 +28,18 @@ name = "uk_mongo_delete_outbox_trigger_domain_origin", columnNames = {"trigger_type", "domain_type", "origin_type", "origin_id"} ) - }, - indexes = { - @Index(name = "idx_mongo_delete_outbox_status_created_at", columnList = "status, created_at"), - @Index(name = "idx_mongo_delete_outbox_status_updated_at", columnList = "status, updated_at") } ) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class MongoDeleteOutbox extends BaseOutboxEntity { +public class MongoDeleteOutbox extends BaseEntity { + + public enum OutboxStatus { + OPEN, + PROCESSING, + DONE, + FAILED + } public enum TriggerType { DELETE, @@ -93,6 +97,24 @@ public enum OriginType { @Column(name = "block_id") private List blockIds; + @Column(nullable = false, length = 32) + @Enumerated(EnumType.STRING) + private OutboxStatus status; + + @Column(nullable = false) + private Integer retryCount; + + @Column(nullable = false) + private Integer maxRetry; + + private LocalDateTime doneAt; + + @Column(length = 2000) + private String lastError; + + @Version + private Long version; + public static MongoDeleteOutbox open( TriggerType triggerType, DomainType domainType, @@ -111,7 +133,36 @@ public static MongoDeleteOutbox open( outbox.saveContentIds = saveIds; outbox.commitBlockSequenceIds = commitIds; outbox.blockIds = blockIds; - outbox.initOutbox(); + outbox.status = OutboxStatus.OPEN; + outbox.retryCount = 0; + outbox.maxRetry = 10; return outbox; } + + public void markProcessing() { + this.status = OutboxStatus.PROCESSING; + updateTimestamp(); + } + + public void markDone() { + this.status = OutboxStatus.DONE; + this.doneAt = LocalDateTime.now(); + this.lastError = null; + updateTimestamp(); + } + + public void markRetry(String errorMessage) { + this.retryCount = this.retryCount + 1; + this.lastError = errorMessage; + + if (this.retryCount >= this.maxRetry) { + this.status = OutboxStatus.FAILED; + updateTimestamp(); + return; + } + + this.status = OutboxStatus.OPEN; + updateTimestamp(); + } + } diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/util/MongoDeleteMapper.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/util/MongoDeleteMapper.java similarity index 86% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/util/MongoDeleteMapper.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/util/MongoDeleteMapper.java index 177f61e7..df23448f 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/util/MongoDeleteMapper.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/util/MongoDeleteMapper.java @@ -1,8 +1,8 @@ -package io.ejangs.docsa.global.outbox.mongo.util; +package io.ejangs.docsa.global.mongo.outbox.util; import io.ejangs.docsa.domain.branch.entity.Branch; import io.ejangs.docsa.domain.save.entity.Save; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; import java.util.List; import java.util.Objects; import java.util.Optional; diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/util/MongoIdsCollector.java b/src/main/java/io/ejangs/docsa/global/mongo/outbox/util/MongoIdsCollector.java similarity index 95% rename from src/main/java/io/ejangs/docsa/global/outbox/mongo/util/MongoIdsCollector.java rename to src/main/java/io/ejangs/docsa/global/mongo/outbox/util/MongoIdsCollector.java index 37d2e67d..ca3c4bba 100644 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/util/MongoIdsCollector.java +++ b/src/main/java/io/ejangs/docsa/global/mongo/outbox/util/MongoIdsCollector.java @@ -1,10 +1,10 @@ -package io.ejangs.docsa.global.outbox.mongo.util; +package io.ejangs.docsa.global.mongo.outbox.util; import io.ejangs.docsa.domain.branch.entity.Branch; import io.ejangs.docsa.domain.commit.dao.mongodb.CommitBlockSequenceRepository; import io.ejangs.docsa.domain.commit.entity.Commit; import io.ejangs.docsa.domain.save.entity.Save; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; import java.util.ArrayList; import java.util.List; import java.util.Objects; diff --git a/src/main/java/io/ejangs/docsa/global/outbox/BaseOutboxEntity.java b/src/main/java/io/ejangs/docsa/global/outbox/BaseOutboxEntity.java deleted file mode 100644 index 21533fa6..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/BaseOutboxEntity.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.ejangs.docsa.global.outbox; - -import io.ejangs.docsa.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.MappedSuperclass; -import jakarta.persistence.Version; -import java.time.LocalDateTime; -import lombok.Getter; - -@Getter -@MappedSuperclass -public abstract class BaseOutboxEntity extends BaseEntity { - - @Column(nullable = false, length = 32) - @Enumerated(EnumType.STRING) - protected OutboxStatus status; - - @Column(nullable = false) - protected Integer retryCount; - - @Column(nullable = false) - protected Integer maxRetry; - - protected LocalDateTime doneAt; - - @Column(length = 2000) - protected String lastError; - - @Version - protected Long version; - - protected void initOutbox() { - this.status = OutboxStatus.OPEN; - this.retryCount = 0; - this.maxRetry = 10; - } - - public void markProcessing() { - this.status = OutboxStatus.PROCESSING; - updateTimestamp(); - } - - public void markDone() { - this.status = OutboxStatus.DONE; - this.doneAt = LocalDateTime.now(); - this.lastError = null; - updateTimestamp(); - } - - public void markRetry(String errorMessage) { - this.retryCount = this.retryCount + 1; - this.lastError = errorMessage; - - if (this.retryCount >= this.maxRetry) { - this.status = OutboxStatus.FAILED; - updateTimestamp(); - return; - } - - this.status = OutboxStatus.OPEN; - updateTimestamp(); - } - - public void recoverProcessingTimeout(String errorMessage) { - this.status = OutboxStatus.OPEN; - this.lastError = errorMessage; - updateTimestamp(); - } -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/OutboxStatus.java b/src/main/java/io/ejangs/docsa/global/outbox/OutboxStatus.java deleted file mode 100644 index 300eec75..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/OutboxStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.ejangs.docsa.global.outbox; - -public enum OutboxStatus { - OPEN, - PROCESSING, - DONE, - FAILED -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/mongo/dao/mysql/MongoDeleteOutboxRepository.java b/src/main/java/io/ejangs/docsa/global/outbox/mongo/dao/mysql/MongoDeleteOutboxRepository.java deleted file mode 100644 index d81c1bd4..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/mongo/dao/mysql/MongoDeleteOutboxRepository.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.ejangs.docsa.global.outbox.mongo.dao.mysql; - -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface MongoDeleteOutboxRepository extends JpaRepository { - - List findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus status); - - List findTop100ByStatusAndUpdatedAtBeforeOrderByUpdatedAtAsc( - OutboxStatus status, - LocalDateTime updatedAt - ); - - Optional findByIdAndStatus(Long id, OutboxStatus status); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query(value = """ - update mongo_delete_outbox - set status = 'PROCESSING', - updated_at = current_timestamp, - version = version + 1 - where id = :outboxId - and status = 'OPEN' - """, nativeQuery = true) - int claimOpenById(@Param("outboxId") Long outboxId); - - Optional findByTriggerTypeAndDomainTypeAndOriginTypeAndOriginId( - TriggerType triggerType, - DomainType domainType, - OriginType originType, - String originId - ); -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxFactory.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxFactory.java deleted file mode 100644 index ec11e2ce..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.app; - -import io.ejangs.docsa.domain.image.entity.Image; -import io.ejangs.docsa.global.outbox.s3.dao.S3DeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.s3.entity.S3DeleteOutbox; -import java.util.Objects; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class S3DeleteOutboxFactory { - - private final S3DeleteOutboxRepository s3DeleteOutboxRepository; - - public void enqueueImageDeletion(Image image) { - Objects.requireNonNull(image, "image is required"); - Objects.requireNonNull(image.getId(), "imageId is required"); - Objects.requireNonNull(image.getObjectKey(), "objectKey is required"); - - image.markDeleting(); - s3DeleteOutboxRepository.insertOpenIfAbsent(image.getId(), image.getObjectKey()); - } -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxLifecycleService.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxLifecycleService.java deleted file mode 100644 index 78fb65e5..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxLifecycleService.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.app; - -import io.ejangs.docsa.domain.image.dao.ImageRepository; -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.s3.dao.S3DeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.s3.dto.S3DeleteTarget; -import io.ejangs.docsa.global.outbox.s3.entity.S3DeleteOutbox; -import java.time.LocalDateTime; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(rollbackFor = Exception.class) -@RequiredArgsConstructor -public class S3DeleteOutboxLifecycleService { - - private final S3DeleteOutboxRepository s3DeleteOutboxRepository; - private final ImageRepository imageRepository; - - public S3DeleteTarget claimOpen(Long outboxId) { - int claimed = s3DeleteOutboxRepository.claimOpenById(outboxId); - if (claimed == 0) { - return null; - } - - S3DeleteOutbox targetOutbox = s3DeleteOutboxRepository - .findByIdAndStatus(outboxId, OutboxStatus.PROCESSING) - .orElse(null); - if (targetOutbox == null) { - return null; - } - - return new S3DeleteTarget(targetOutbox.getImageId(), targetOutbox.getObjectKey()); - } - - public void done(Long outboxId) { - S3DeleteOutbox targetOutbox = s3DeleteOutboxRepository - .findByIdAndStatus(outboxId, OutboxStatus.PROCESSING) - .orElse(null); - if (targetOutbox == null) { - return; - } - - targetOutbox.markDone(); - s3DeleteOutboxRepository.save(targetOutbox); - imageRepository.findById(targetOutbox.getImageId()) - .ifPresent(image -> { - image.markDeleted(); - imageRepository.save(image); - }); - } - - public void retry(Long outboxId, String errorMessage) { - S3DeleteOutbox targetOutbox = s3DeleteOutboxRepository - .findByIdAndStatus(outboxId, OutboxStatus.PROCESSING) - .orElse(null); - if (targetOutbox == null) { - return; - } - - targetOutbox.markRetry(errorMessage); - s3DeleteOutboxRepository.save(targetOutbox); - } - - public int recoverTimedOutProcessing(LocalDateTime threshold) { - List stuckOutboxes = s3DeleteOutboxRepository - .findTop100ByStatusAndUpdatedAtBeforeOrderByUpdatedAtAsc( - OutboxStatus.PROCESSING, - threshold - ); - if (stuckOutboxes.isEmpty()) { - return 0; - } - - stuckOutboxes.forEach(outbox -> outbox.recoverProcessingTimeout("PROCESSING timeout recovered")); - s3DeleteOutboxRepository.saveAll(stuckOutboxes); - return stuckOutboxes.size(); - } -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxWorker.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxWorker.java deleted file mode 100644 index 006b9aaa..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteOutboxWorker.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.app; - -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.s3.dao.S3DeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.s3.dto.S3DeleteTarget; -import io.ejangs.docsa.global.outbox.s3.entity.S3DeleteOutbox; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -@ConditionalOnProperty( - prefix = "s3.delete.outbox.worker", - name = "enabled", - havingValue = "true", - matchIfMissing = true -) -public class S3DeleteOutboxWorker { - - private static final Duration PROCESSING_TIMEOUT = Duration.ofMinutes(5); - - private final S3DeleteOutboxRepository s3DeleteOutboxRepository; - private final S3DeleteService s3DeleteService; - private final S3DeleteOutboxLifecycleService s3DeleteOutboxLifecycleService; - - @Scheduled( - fixedDelayString = "${s3.delete.outbox.worker.fixed-delay:PT1M}", - initialDelayString = "${s3.delete.outbox.worker.initial-delay:PT0S}" - ) - public void run() { - int recovered = s3DeleteOutboxLifecycleService.recoverTimedOutProcessing( - LocalDateTime.now().minus(PROCESSING_TIMEOUT) - ); - if (recovered > 0) { - log.warn("[S3 Outbox Worker] Recovered timed-out PROCESSING rows: {}", recovered); - } - - List outboxes = s3DeleteOutboxRepository - .findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus.OPEN); - if (outboxes.isEmpty()) { - return; - } - - deleteTargets(outboxes); - log.info("[S3 Outbox Worker] Complete target delete : {}", outboxes.size()); - } - - private void deleteTargets(List outboxes) { - for (S3DeleteOutbox outbox : outboxes) { - S3DeleteTarget target = s3DeleteOutboxLifecycleService.claimOpen(outbox.getId()); - if (target == null) { - continue; - } - - try { - s3DeleteService.deleteTarget(target); - s3DeleteOutboxLifecycleService.done(outbox.getId()); - } catch (Exception e) { - log.error("[S3 Outbox Worker] Error : {}", e.getMessage(), e); - s3DeleteOutboxLifecycleService.retry(outbox.getId(), e.getMessage()); - } - } - } -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteService.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteService.java deleted file mode 100644 index f3f6f383..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteService.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.app; - -import io.ejangs.docsa.global.outbox.s3.dto.S3DeleteTarget; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.S3Exception; - -@Service -@RequiredArgsConstructor -public class S3DeleteService { - - private final S3Client s3Client; - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - public void deleteTarget(S3DeleteTarget target) { - try { - s3Client.deleteObject(DeleteObjectRequest.builder() - .bucket(bucket) - .key(target.objectKey()) - .build()); - } catch (S3Exception e) { - if (e.statusCode() == 404) { - return; - } - throw e; - } - } -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/dao/S3DeleteOutboxRepository.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/dao/S3DeleteOutboxRepository.java deleted file mode 100644 index cff592ae..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/dao/S3DeleteOutboxRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.dao; - -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.s3.entity.S3DeleteOutbox; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface S3DeleteOutboxRepository extends JpaRepository { - - List findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus status); - - List findTop100ByStatusAndUpdatedAtBeforeOrderByUpdatedAtAsc( - OutboxStatus status, - LocalDateTime updatedAt - ); - - Optional findByIdAndStatus(Long id, OutboxStatus status); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query(value = """ - update s3_delete_outbox - set status = 'PROCESSING', - updated_at = current_timestamp, - version = version + 1 - where id = :outboxId - and status = 'OPEN' - """, nativeQuery = true) - int claimOpenById(@Param("outboxId") Long outboxId); - - @Modifying - @Query(value = """ - insert into s3_delete_outbox - (created_at, updated_at, image_id, object_key, status, retry_count, max_retry, version) - values - (now(6), now(6), :imageId, :objectKey, 'OPEN', 0, 10, 0) - on duplicate key update - object_key = object_key - """, nativeQuery = true) - void insertOpenIfAbsent(@Param("imageId") Long imageId, @Param("objectKey") String objectKey); -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/dto/S3DeleteTarget.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/dto/S3DeleteTarget.java deleted file mode 100644 index f65af433..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/dto/S3DeleteTarget.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.dto; - -public record S3DeleteTarget( - Long imageId, - String objectKey -) { -} diff --git a/src/main/java/io/ejangs/docsa/global/outbox/s3/entity/S3DeleteOutbox.java b/src/main/java/io/ejangs/docsa/global/outbox/s3/entity/S3DeleteOutbox.java deleted file mode 100644 index 00efb477..00000000 --- a/src/main/java/io/ejangs/docsa/global/outbox/s3/entity/S3DeleteOutbox.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.entity; - -import io.ejangs.docsa.global.outbox.BaseOutboxEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Index; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table( - name = "s3_delete_outbox", - uniqueConstraints = { - @UniqueConstraint(name = "uk_s3_delete_outbox_object_key", columnNames = "object_key") - }, - indexes = { - @Index(name = "idx_s3_delete_outbox_status_created_at", columnList = "status, created_at"), - @Index(name = "idx_s3_delete_outbox_status_updated_at", columnList = "status, updated_at") - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class S3DeleteOutbox extends BaseOutboxEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private Long imageId; - - @Column(name = "object_key", nullable = false, length = 500) - private String objectKey; - - public static S3DeleteOutbox open(Long imageId, String objectKey) { - S3DeleteOutbox outbox = new S3DeleteOutbox(); - outbox.imageId = imageId; - outbox.objectKey = objectKey; - outbox.initOutbox(); - return outbox; - } -} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 0b843e06..6ff7c820 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -16,24 +16,19 @@ spring: starttls-enable: ${STARTTLS_ENABLE:false} starttls-required: ${STARTTLS_REQUIRED:false} - flyway: - enabled: ${FLYWAY_ENABLED:false} - baseline-on-migrate: ${FLYWAY_BASELINE_ON_MIGRATE:false} - baseline-version: ${FLYWAY_BASELINE_VERSION:1} - jpa: hibernate: - ddl-auto: ${DDL_AUTO:create-drop} + ddl-auto: create-drop properties: hibernate: show_sql: false - format_sql: false - highlight_sql: false + format_sql: true + highlight_sql: true decorator: datasource: datasource-proxy: - enabled: false + enabled: true logging: slf4j multiline: true format-sql: true @@ -48,7 +43,7 @@ decorator: logging: level: - net.ttddyy.dsproxy.listener: OFF + net.ttddyy.dsproxy.listener: DEBUG server: port: 8080 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 91e8e597..5c239b98 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -5,11 +5,6 @@ spring: username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} - flyway: - enabled: ${FLYWAY_ENABLED:true} - baseline-on-migrate: ${FLYWAY_BASELINE_ON_MIGRATE:true} - baseline-version: ${FLYWAY_BASELINE_VERSION:1} - mail: host: smtp.gmail.com port: 587 diff --git a/src/main/resources/application-stg.yml b/src/main/resources/application-stg.yml index 18c0faf4..eb44e05b 100644 --- a/src/main/resources/application-stg.yml +++ b/src/main/resources/application-stg.yml @@ -5,11 +5,6 @@ spring: username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} - flyway: - enabled: ${FLYWAY_ENABLED:true} - baseline-on-migrate: ${FLYWAY_BASELINE_ON_MIGRATE:true} - baseline-version: ${FLYWAY_BASELINE_VERSION:1} - mail: host: mailpit port: 1025 @@ -77,13 +72,18 @@ springdoc: decorator: datasource: datasource-proxy: - enabled: false + enabled: true + logging: slf4j + multiline: true + format-sql: true + count-query: true query: - enable-logging: false + enable-logging: true + log-level: debug slow-query: enable-logging: true log-level: warn - threshold: 300 + threshold: 100 logging: level: diff --git a/src/main/resources/db/migration/V1__init_schema.sql b/src/main/resources/db/migration/V1__init_schema.sql deleted file mode 100644 index f353118b..00000000 --- a/src/main/resources/db/migration/V1__init_schema.sql +++ /dev/null @@ -1,142 +0,0 @@ -SET FOREIGN_KEY_CHECKS = 0; - -CREATE TABLE `branches` ( - `created_at` datetime(6) NOT NULL, - `document_id` bigint DEFAULT NULL, - `from_commit_id` bigint DEFAULT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `leaf_commit_id` bigint DEFAULT NULL, - `merge_target_commit_id` bigint DEFAULT NULL, - `root_commit_id` bigint DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `name` varchar(100) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `UKf6lkgdoc9cekvllr7juun2571` (`name`,`document_id`), - UNIQUE KEY `UK3xq7lqgyigivvjp137bq1vmqp` (`leaf_commit_id`), - UNIQUE KEY `UKhr8mfwhefxqiel77vtf8swma0` (`root_commit_id`), - KEY `FKeltapt1yxecp73acaguheypy` (`document_id`), - KEY `idx_branches_from_commit_id` (`from_commit_id`), - KEY `idx_branches_merge_target_commit_id` (`merge_target_commit_id`), - CONSTRAINT `FK7twh77wcak2w54m32nsy2363` FOREIGN KEY (`root_commit_id`) REFERENCES `commits` (`id`), - CONSTRAINT `FKa0eeai0ufc7if1gjjnegbx47o` FOREIGN KEY (`leaf_commit_id`) REFERENCES `commits` (`id`), - CONSTRAINT `FKeltapt1yxecp73acaguheypy` FOREIGN KEY (`document_id`) REFERENCES `docs` (`id`), - CONSTRAINT `FKmtx5jtry1cln0u3hdlb4m755t` FOREIGN KEY (`merge_target_commit_id`) REFERENCES `commits` (`id`), - CONSTRAINT `FKs3sjpir7sqlh1i56lo0qkeks9` FOREIGN KEY (`from_commit_id`) REFERENCES `commits` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `commits` ( - `branch_id` bigint DEFAULT NULL, - `created_at` datetime(6) NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `commit_mongo_id` varchar(255) DEFAULT NULL, - `description` varchar(255) DEFAULT NULL, - `title` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `FKnt8u1dar544lhedn95o9eerl7` (`branch_id`), - CONSTRAINT `FKnt8u1dar544lhedn95o9eerl7` FOREIGN KEY (`branch_id`) REFERENCES `branches` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `docs` ( - `created_at` datetime(6) NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `title` varchar(50) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_title` (`user_id`,`title`), - CONSTRAINT `FK9tkihf94m82526acn683ihkdc` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `edges` ( - `document_id` bigint NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `next_commit_id` bigint NOT NULL, - `prev_commit_id` bigint NOT NULL, - PRIMARY KEY (`id`), - KEY `FKgs0qaalf21ufuu813879c2xc0` (`document_id`), - KEY `FKnwxobo0keg8te2ynwp094kwqp` (`next_commit_id`), - KEY `FKq7cxggirphdswwud6pb6y3uki` (`prev_commit_id`), - CONSTRAINT `FKgs0qaalf21ufuu813879c2xc0` FOREIGN KEY (`document_id`) REFERENCES `docs` (`id`), - CONSTRAINT `FKnwxobo0keg8te2ynwp094kwqp` FOREIGN KEY (`next_commit_id`) REFERENCES `commits` (`id`), - CONSTRAINT `FKq7cxggirphdswwud6pb6y3uki` FOREIGN KEY (`prev_commit_id`) REFERENCES `commits` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `images` ( - `created_at` datetime(6) NOT NULL, - `doc_id` bigint NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `size` bigint NOT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `object_key` varchar(500) NOT NULL, - `content_type` varchar(255) NOT NULL, - `original_file_name` varchar(255) NOT NULL, - `status` enum('ACTIVE','FAILED','PENDING') NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_images_object_key` (`object_key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `mongo_delete_outbox` ( - `max_retry` int NOT NULL, - `retry_count` int NOT NULL, - `created_at` datetime(6) NOT NULL, - `done_at` datetime(6) DEFAULT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `version` bigint DEFAULT NULL, - `last_error` varchar(2000) DEFAULT NULL, - `origin_id` varchar(255) NOT NULL, - `domain_type` enum('BRANCH','COMMIT','DOC','MERGE','SAVE') NOT NULL, - `origin_type` enum('BRANCH_ID','CBS_ID','COMMIT_ID','DOC_ID','SAVE_CONTENT_ID','SAVE_ID') NOT NULL, - `status` enum('DONE','FAILED','OPEN','PROCESSING') NOT NULL, - `trigger_type` enum('COMPENSATE','DELETE') NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_mongo_delete_outbox_trigger_domain_origin` (`trigger_type`,`domain_type`,`origin_type`,`origin_id`), - KEY `idx_mongo_delete_outbox_status_created_at` (`status`,`created_at`), - KEY `idx_mongo_delete_outbox_status_updated_at` (`status`,`updated_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `mongo_outbox_block_ids` ( - `outbox_id` bigint NOT NULL, - `block_id` varchar(255) DEFAULT NULL, - KEY `fk_mongo_outbox_block_ids_outbox` (`outbox_id`), - CONSTRAINT `fk_mongo_outbox_block_ids_outbox` FOREIGN KEY (`outbox_id`) REFERENCES `mongo_delete_outbox` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `mongo_outbox_commit_ids` ( - `outbox_id` bigint NOT NULL, - `commit_id` varchar(255) DEFAULT NULL, - KEY `fk_mongo_outbox_commit_ids_outbox` (`outbox_id`), - CONSTRAINT `fk_mongo_outbox_commit_ids_outbox` FOREIGN KEY (`outbox_id`) REFERENCES `mongo_delete_outbox` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `mongo_outbox_save_ids` ( - `outbox_id` bigint NOT NULL, - `save_id` varchar(255) DEFAULT NULL, - KEY `fk_mongo_outbox_save_ids_outbox` (`outbox_id`), - CONSTRAINT `fk_mongo_outbox_save_ids_outbox` FOREIGN KEY (`outbox_id`) REFERENCES `mongo_delete_outbox` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `saves` ( - `branch_id` bigint NOT NULL, - `created_at` datetime(6) NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `save_mongo_id` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `UKc6xd7j1suin26hn2oghr6tkvl` (`branch_id`), - CONSTRAINT `FK9s60khkapgaopi0la134u25a2` FOREIGN KEY (`branch_id`) REFERENCES `branches` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE `users` ( - `created_at` datetime(6) NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `email` varchar(255) NOT NULL, - `name` varchar(255) NOT NULL, - `password` varchar(255) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/resources/db/migration/V2__add_thumbnail_and_s3_delete_outbox.sql b/src/main/resources/db/migration/V2__add_thumbnail_and_s3_delete_outbox.sql deleted file mode 100644 index a6be1ba1..00000000 --- a/src/main/resources/db/migration/V2__add_thumbnail_and_s3_delete_outbox.sql +++ /dev/null @@ -1,80 +0,0 @@ -ALTER TABLE images - ADD COLUMN purpose enum('DOC_CONTENT','DOC_THUMBNAIL') NOT NULL DEFAULT 'DOC_CONTENT' - AFTER original_file_name; - -ALTER TABLE images - MODIFY COLUMN status enum('ACTIVE','DELETED','DELETING','FAILED','PENDING') NOT NULL; - -SET @idx_exists = ( - SELECT COUNT(*) - FROM information_schema.statistics - WHERE table_schema = DATABASE() - AND table_name = 'mongo_delete_outbox' - AND index_name = 'idx_mongo_delete_outbox_status_created_at' -); -SET @sql = IF( - @idx_exists = 0, - 'CREATE INDEX idx_mongo_delete_outbox_status_created_at ON mongo_delete_outbox (status, created_at)', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - -SET @idx_exists = ( - SELECT COUNT(*) - FROM information_schema.statistics - WHERE table_schema = DATABASE() - AND table_name = 'mongo_delete_outbox' - AND index_name = 'idx_mongo_delete_outbox_status_updated_at' -); -SET @sql = IF( - @idx_exists = 0, - 'CREATE INDEX idx_mongo_delete_outbox_status_updated_at ON mongo_delete_outbox (status, updated_at)', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - -CREATE TABLE doc_thumbnails ( - created_at datetime(6) NOT NULL, - current_image_id bigint DEFAULT NULL, - doc_id bigint NOT NULL, - generated_at datetime(6) DEFAULT NULL, - id bigint NOT NULL AUTO_INCREMENT, - request_token bigint NOT NULL, - requested_at datetime(6) DEFAULT NULL, - updated_at datetime(6) DEFAULT NULL, - version bigint DEFAULT NULL, - last_error varchar(500) DEFAULT NULL, - signature varchar(255) DEFAULT NULL, - status enum('EMPTY','FAILED','PENDING','READY') NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uk_doc_thumbnails_doc_id (doc_id), - UNIQUE KEY uk_doc_thumbnails_current_image_id (current_image_id), - CONSTRAINT fk_doc_thumbnails_current_image - FOREIGN KEY (current_image_id) REFERENCES images (id), - CONSTRAINT fk_doc_thumbnails_doc - FOREIGN KEY (doc_id) REFERENCES docs (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE s3_delete_outbox ( - max_retry int NOT NULL, - retry_count int NOT NULL, - created_at datetime(6) NOT NULL, - done_at datetime(6) DEFAULT NULL, - id bigint NOT NULL AUTO_INCREMENT, - image_id bigint NOT NULL, - updated_at datetime(6) DEFAULT NULL, - version bigint DEFAULT NULL, - object_key varchar(500) NOT NULL, - last_error varchar(2000) DEFAULT NULL, - status enum('DONE','FAILED','OPEN','PROCESSING') NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uk_s3_delete_outbox_object_key (object_key), - KEY idx_s3_delete_outbox_status_created_at (status, created_at), - KEY idx_s3_delete_outbox_status_updated_at (status, updated_at), - CONSTRAINT fk_s3_delete_outbox_image - FOREIGN KEY (image_id) REFERENCES images (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/src/test/java/io/ejangs/docsa/DocsaApplicationTests.java b/src/test/java/io/ejangs/docsa/DocsaApplicationTests.java index a7131403..5c66478d 100644 --- a/src/test/java/io/ejangs/docsa/DocsaApplicationTests.java +++ b/src/test/java/io/ejangs/docsa/DocsaApplicationTests.java @@ -2,11 +2,13 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.test.context.ActiveProfiles; @EnableAsync +@EnableRetry @EnableScheduling @SpringBootTest @ActiveProfiles("test") diff --git a/src/test/java/io/ejangs/docsa/domain/branch/app/BranchServiceTest.java b/src/test/java/io/ejangs/docsa/domain/branch/app/BranchServiceTest.java index db23d3be..c94e1da6 100644 --- a/src/test/java/io/ejangs/docsa/domain/branch/app/BranchServiceTest.java +++ b/src/test/java/io/ejangs/docsa/domain/branch/app/BranchServiceTest.java @@ -17,11 +17,11 @@ import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateConsistencyIntegrationTest.java b/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateConsistencyIntegrationTest.java index 7917438d..7a625aa5 100644 --- a/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateConsistencyIntegrationTest.java +++ b/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateConsistencyIntegrationTest.java @@ -27,11 +27,11 @@ import io.ejangs.docsa.domain.user.entity.User; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; import java.util.HashSet; import java.util.List; import java.util.Map; diff --git a/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestratorTest.java b/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestratorTest.java index 092ba87a..de8aff36 100644 --- a/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestratorTest.java +++ b/src/test/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestratorTest.java @@ -15,11 +15,11 @@ import io.ejangs.docsa.domain.doc.entity.Doc; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/io/ejangs/docsa/domain/commit/app/CommitCreateOrchestratorTest.java b/src/test/java/io/ejangs/docsa/domain/commit/app/CommitCreateOrchestratorTest.java index c0e3ca37..dc0dda37 100644 --- a/src/test/java/io/ejangs/docsa/domain/commit/app/CommitCreateOrchestratorTest.java +++ b/src/test/java/io/ejangs/docsa/domain/commit/app/CommitCreateOrchestratorTest.java @@ -17,11 +17,11 @@ import io.ejangs.docsa.domain.doc.entity.Doc; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.CommitErrorCode; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; import java.util.Collections; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/ejangs/docsa/domain/commit/app/CommitServiceMockTest.java b/src/test/java/io/ejangs/docsa/domain/commit/app/CommitServiceMockTest.java index a8f4a926..51a2c073 100644 --- a/src/test/java/io/ejangs/docsa/domain/commit/app/CommitServiceMockTest.java +++ b/src/test/java/io/ejangs/docsa/domain/commit/app/CommitServiceMockTest.java @@ -25,7 +25,7 @@ import io.ejangs.docsa.domain.user.security.CustomUserDetails; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BlockSequenceErrorCode; -import io.ejangs.docsa.global.outbox.mongo.util.MongoIdsCollector; +import io.ejangs.docsa.global.mongo.outbox.util.MongoIdsCollector; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/io/ejangs/docsa/domain/commit/app/CreateCommitIntegrationTest.java b/src/test/java/io/ejangs/docsa/domain/commit/app/CreateCommitIntegrationTest.java index 1375ffa7..760a786c 100644 --- a/src/test/java/io/ejangs/docsa/domain/commit/app/CreateCommitIntegrationTest.java +++ b/src/test/java/io/ejangs/docsa/domain/commit/app/CreateCommitIntegrationTest.java @@ -31,9 +31,8 @@ import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.BlockSequenceErrorCode; import io.ejangs.docsa.global.exception.errorcode.CommitErrorCode; -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -241,7 +240,7 @@ void mysqlFail_compensateMongoDelete() { assertThat(outbox.getTriggerType()).isEqualTo(MongoDeleteOutbox.TriggerType.COMPENSATE); assertThat(outbox.getDomainType()).isEqualTo(MongoDeleteOutbox.DomainType.COMMIT); assertThat(outbox.getOriginType()).isEqualTo(MongoDeleteOutbox.OriginType.CBS_ID); - assertThat(outbox.getStatus()).isEqualTo(OutboxStatus.OPEN); + assertThat(outbox.getStatus()).isEqualTo(MongoDeleteOutbox.OutboxStatus.OPEN); assertThat(outbox.getOriginId()).isNotBlank(); }); } diff --git a/src/test/java/io/ejangs/docsa/domain/commit/app/DeleteCommitIntegrationTest.java b/src/test/java/io/ejangs/docsa/domain/commit/app/DeleteCommitIntegrationTest.java index 0d9678a2..2214ae3f 100644 --- a/src/test/java/io/ejangs/docsa/domain/commit/app/DeleteCommitIntegrationTest.java +++ b/src/test/java/io/ejangs/docsa/domain/commit/app/DeleteCommitIntegrationTest.java @@ -21,8 +21,8 @@ import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.CommitErrorCode; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.util.MongoIdsCollector; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.util.MongoIdsCollector; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/io/ejangs/docsa/domain/doc/integration/DocServiceIntegrationTests.java b/src/test/java/io/ejangs/docsa/domain/doc/integration/DocServiceIntegrationTests.java index d0f6d550..db559482 100644 --- a/src/test/java/io/ejangs/docsa/domain/doc/integration/DocServiceIntegrationTests.java +++ b/src/test/java/io/ejangs/docsa/domain/doc/integration/DocServiceIntegrationTests.java @@ -23,7 +23,6 @@ import io.ejangs.docsa.domain.doc.dto.response.DocPageResponse; import io.ejangs.docsa.domain.doc.dto.response.DocSimplePageResponse; import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; import io.ejangs.docsa.domain.doc.util.DocTestUtils; import io.ejangs.docsa.domain.save.dao.mongodb.SaveContentRepository; import io.ejangs.docsa.domain.save.dao.mysql.SaveRepository; @@ -36,9 +35,8 @@ import io.ejangs.docsa.global.exception.errorcode.DatabaseErrorCode; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; import io.ejangs.docsa.global.exception.errorcode.UserErrorCode; -import io.ejangs.docsa.global.outbox.OutboxStatus; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -214,7 +212,7 @@ void mysqlFail_createCompensateOutbox() { assertThat(outbox.getTriggerType()).isEqualTo(MongoDeleteOutbox.TriggerType.COMPENSATE); assertThat(outbox.getDomainType()).isEqualTo(MongoDeleteOutbox.DomainType.DOC); assertThat(outbox.getOriginType()).isEqualTo(MongoDeleteOutbox.OriginType.SAVE_CONTENT_ID); - assertThat(outbox.getStatus()).isEqualTo(OutboxStatus.OPEN); + assertThat(outbox.getStatus()).isEqualTo(MongoDeleteOutbox.OutboxStatus.OPEN); } } @Nested @@ -252,7 +250,7 @@ void mysqlFail_storeOpenOutbox() { List outboxes = mongoDeleteOutboxRepository.findAll(); assertThat(outboxes).hasSize(1); - assertThat(outboxes.getFirst().getStatus()).isEqualTo(OutboxStatus.OPEN); + assertThat(outboxes.getFirst().getStatus()).isEqualTo(MongoDeleteOutbox.OutboxStatus.OPEN); assertThat(outboxes.getFirst().getRetryCount()).isEqualTo(0); } } @@ -341,11 +339,11 @@ void getDocListWithPreview() throws Exception { assertEquals("문서 1", second.title()); assertEquals(RecentType.COMMIT, second.recent().recentType()); - assertEquals(ThumbnailStatus.EMPTY, second.thumbnailStatus()); + assertTrue(second.preview().startsWith("문단 5: 몰라어쩌구저꺼궁롱ㄹ라알이;ㅇㄹ")); // preview 포함 assertEquals("문서 2", first.title()); assertEquals(RecentType.SAVE, first.recent().recentType()); - assertEquals(ThumbnailStatus.EMPTY, first.thumbnailStatus()); + assertTrue(first.preview().startsWith("문단 3: 테스트 코드가 너무 싫어서 미치겠다는 문단")); // preview 포함 } @Test diff --git a/src/test/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailServiceUnitTest.java b/src/test/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailServiceUnitTest.java deleted file mode 100644 index eda7b8a1..00000000 --- a/src/test/java/io/ejangs/docsa/domain/doc/thumbnail/app/ThumbnailServiceUnitTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package io.ejangs.docsa.domain.doc.thumbnail.app; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.ejangs.docsa.domain.doc.app.create.DocQueryService; -import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailResponse; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail; -import io.ejangs.docsa.domain.image.app.ImageQueryService; -import io.ejangs.docsa.domain.image.entity.Image; -import io.ejangs.docsa.domain.image.entity.Image.ImageStatus; -import io.ejangs.docsa.domain.image.entity.Image.Purpose; -import io.ejangs.docsa.global.outbox.s3.app.S3DeleteOutboxFactory; -import io.ejangs.docsa.global.outbox.s3.dao.S3DeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.s3.entity.S3DeleteOutbox; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -@ExtendWith(MockitoExtension.class) -class ThumbnailServiceUnitTest { - - @Mock - private ThumbnailQueryService thumbnailQueryService; - - @Mock - private DocQueryService docQueryService; - - @Mock - private ImageQueryService imageQueryService; - - @Mock - private S3DeleteOutboxRepository s3DeleteOutboxRepository; - - private ThumbnailService thumbnailService; - - @BeforeEach - void setUp() { - S3DeleteOutboxFactory s3DeleteOutboxFactory = - new S3DeleteOutboxFactory(s3DeleteOutboxRepository); - thumbnailService = new ThumbnailService( - thumbnailQueryService, - docQueryService, - imageQueryService, - s3DeleteOutboxFactory - ); - ReflectionTestUtils.setField(thumbnailService, "cdnUrl", "https://cdn.example.com"); - } - - @Test - @DisplayName("기존 썸네일을 새 이미지로 교체하면 이전 S3 객체 삭제 Outbox를 적재한다") - void finalizeThumbnail_enqueuePreviousThumbnailDeletion() { - Long userId = 1L; - Long docId = 2L; - Long requestToken = 1L; - Image oldImage = activeThumbnailImage(10L, userId, docId, "old.webp"); - Image newImage = activeThumbnailImage(11L, userId, docId, "new.webp"); - Thumbnail thumbnail = thumbnailWithCurrentImage(oldImage, requestToken); - - when(thumbnailQueryService.getByDocIdForUpdate(docId)).thenReturn(thumbnail); - when(imageQueryService.getByIdAndUserId(newImage.getId(), userId)).thenReturn(newImage); - - ThumbnailResponse response = thumbnailService.finalizeThumbnail( - userId, - docId, - newImage.getId(), - requestToken, - "new-signature" - ); - - assertThat(response.imageId()).isEqualTo(newImage.getId()); - assertThat(response.thumbnailUrl()).isEqualTo("https://cdn.example.com/" + newImage.getObjectKey()); - assertThat(thumbnail.getCurrentImage()).isEqualTo(newImage); - assertThat(oldImage.getStatus()).isEqualTo(ImageStatus.DELETING); - verify(s3DeleteOutboxRepository).insertOpenIfAbsent( - 10L, - "users/1/docs/2/images/old.webp" - ); - } - - @Test - @DisplayName("같은 이미지를 다시 확정하면 삭제 Outbox를 만들지 않는다") - void finalizeThumbnail_skipDeletionWhenImageIsSame() { - Long userId = 1L; - Long docId = 2L; - Long requestToken = 1L; - Image image = activeThumbnailImage(10L, userId, docId, "same.webp"); - Thumbnail thumbnail = thumbnailWithCurrentImage(image, requestToken); - - when(thumbnailQueryService.getByDocIdForUpdate(docId)).thenReturn(thumbnail); - when(imageQueryService.getByIdAndUserId(image.getId(), userId)).thenReturn(image); - - thumbnailService.finalizeThumbnail( - userId, - docId, - image.getId(), - requestToken, - "same-signature" - ); - - assertThat(image.getStatus()).isEqualTo(ImageStatus.ACTIVE); - verify(s3DeleteOutboxRepository, never()).save(any(S3DeleteOutbox.class)); - } - - private Thumbnail thumbnailWithCurrentImage(Image image, Long expectedRequestToken) { - Thumbnail thumbnail = Thumbnail.builder() - .doc(mock(Doc.class)) - .build(); - thumbnail.complete(image, "old-signature"); - Long requestToken = thumbnail.requestUpdate(); - assertThat(requestToken).isEqualTo(expectedRequestToken); - return thumbnail; - } - - private Image activeThumbnailImage(Long imageId, Long userId, Long docId, String fileName) { - Image image = Image.builder() - .userId(userId) - .docId(docId) - .originalFileName(fileName) - .objectKey("users/%d/docs/%d/images/%s".formatted(userId, docId, fileName)) - .contentType("image/webp") - .size(1024L) - .purpose(Purpose.DOC_THUMBNAIL) - .build(); - ReflectionTestUtils.setField(image, "id", imageId); - image.activate(); - return image; - } -} diff --git a/src/test/java/io/ejangs/docsa/domain/doc/unit/DocCreateOrchestratorTest.java b/src/test/java/io/ejangs/docsa/domain/doc/unit/DocCreateOrchestratorTest.java index 46d107fa..cbd2c347 100644 --- a/src/test/java/io/ejangs/docsa/domain/doc/unit/DocCreateOrchestratorTest.java +++ b/src/test/java/io/ejangs/docsa/domain/doc/unit/DocCreateOrchestratorTest.java @@ -18,10 +18,10 @@ import io.ejangs.docsa.domain.user.entity.User; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.app.MongoDeleteOutboxFactory; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/io/ejangs/docsa/domain/doc/unit/DocServiceUnitTests.java b/src/test/java/io/ejangs/docsa/domain/doc/unit/DocServiceUnitTests.java index b1d3312b..53caca4e 100644 --- a/src/test/java/io/ejangs/docsa/domain/doc/unit/DocServiceUnitTests.java +++ b/src/test/java/io/ejangs/docsa/domain/doc/unit/DocServiceUnitTests.java @@ -35,7 +35,7 @@ import io.ejangs.docsa.domain.user.entity.User; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.DocErrorCode; -import io.ejangs.docsa.global.outbox.mongo.util.MongoIdsCollector; +import io.ejangs.docsa.global.mongo.outbox.util.MongoIdsCollector; import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; diff --git a/src/test/java/io/ejangs/docsa/domain/doc/util/DocTestUtils.java b/src/test/java/io/ejangs/docsa/domain/doc/util/DocTestUtils.java index f0d01669..6cb0f100 100644 --- a/src/test/java/io/ejangs/docsa/domain/doc/util/DocTestUtils.java +++ b/src/test/java/io/ejangs/docsa/domain/doc/util/DocTestUtils.java @@ -13,7 +13,6 @@ import io.ejangs.docsa.domain.doc.dto.RecentActivityDto.RecentType; import io.ejangs.docsa.domain.doc.dto.response.DocPageResponse; import io.ejangs.docsa.domain.doc.entity.Doc; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; import io.ejangs.docsa.domain.edge.entity.Edge; import io.ejangs.docsa.domain.save.dao.mongodb.SaveContentRepository; import io.ejangs.docsa.domain.save.document.SaveContent; @@ -366,7 +365,7 @@ public static Page convertToDocListResponsePage(List docs, String title = doc.getTitle(); LocalDateTime createdAt = doc.getCreatedAt(); LocalDateTime updatedAt = doc.getUpdatedAt(); - String thumbnailUrl = null; + String preview = "미리보기 없음"; // 최근 활동 (SAVE > COMMIT 우선) RecentActivityDto recent = doc.getBranches().stream() @@ -388,8 +387,7 @@ public static Page convertToDocListResponsePage(List docs, .findFirst() .orElse(null); - return new DocPageResponse(docId, title, createdAt, updatedAt, thumbnailUrl, - ThumbnailStatus.EMPTY, recent); + return new DocPageResponse(docId, title, createdAt, updatedAt, preview, recent); }) .toList(); diff --git a/src/test/java/io/ejangs/docsa/domain/image/app/ImageServiceUnitTest.java b/src/test/java/io/ejangs/docsa/domain/image/app/ImageServiceUnitTest.java index fbc9f852..ee585284 100644 --- a/src/test/java/io/ejangs/docsa/domain/image/app/ImageServiceUnitTest.java +++ b/src/test/java/io/ejangs/docsa/domain/image/app/ImageServiceUnitTest.java @@ -16,11 +16,11 @@ import io.ejangs.docsa.domain.image.dto.response.ImageUploadUrlResponse; import io.ejangs.docsa.domain.image.entity.Image; import io.ejangs.docsa.domain.image.entity.Image.ImageStatus; -import io.ejangs.docsa.domain.image.entity.Image.Purpose; import io.ejangs.docsa.global.exception.CustomException; import io.ejangs.docsa.global.exception.errorcode.ImageErrorCode; import java.net.URI; import java.time.Duration; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -44,9 +44,6 @@ class ImageServiceUnitTest { @Mock private ImageRepository imageRepository; - @Mock - private ImageQueryService imageQueryService; - @Mock private DocQueryService docQueryService; @@ -63,8 +60,7 @@ class ImageServiceUnitTest { @BeforeEach void setUp() { - imageService = new ImageService(imageRepository, imageQueryService, docQueryService, - s3Presigner, s3Client); + imageService = new ImageService(imageRepository, docQueryService, s3Presigner, s3Client); ReflectionTestUtils.setField(imageService, "bucket", "docsa-image-bucket"); ReflectionTestUtils.setField(imageService, "expireMinutes", 5L); ReflectionTestUtils.setField(imageService, "cdnUrl", "https://cdn.example.com"); @@ -76,7 +72,7 @@ void createUploadUrl_success() throws Exception { Long userId = 1L; Long docId = 2L; ImageUploadUrlRequest request = - new ImageUploadUrlRequest(docId, "sample.png", "image/png", 1024L, Purpose.DOC_CONTENT); + new ImageUploadUrlRequest(docId, "sample.png", "image/png", 1024L); when(docQueryService.getByIdAndUserId(docId, userId)).thenReturn(org.mockito.Mockito.mock(Doc.class)); when(imageRepository.save(any(Image.class))).thenAnswer(invocation -> { @@ -111,47 +107,13 @@ void createUploadUrl_success() throws Exception { assertThat(putObjectRequest.contentType()).isEqualTo("image/png"); } - @Test - @DisplayName("썸네일 업로드 URL 생성 시 thumbnails prefix로 S3 key를 만든다") - void createUploadUrl_success_whenPurposeIsThumbnail() throws Exception { - Long userId = 1L; - Long docId = 2L; - ImageUploadUrlRequest request = - new ImageUploadUrlRequest(docId, "thumbnail.webp", "image/webp", 1024L, Purpose.DOC_THUMBNAIL); - - when(docQueryService.getByIdAndUserId(docId, userId)).thenReturn(org.mockito.Mockito.mock(Doc.class)); - when(imageRepository.save(any(Image.class))).thenAnswer(invocation -> { - Image image = invocation.getArgument(0); - ReflectionTestUtils.setField(image, "id", 10L); - return image; - }); - when(presignedPutObjectRequest.url()) - .thenReturn(URI.create("https://s3.example.com/upload").toURL()); - when(s3Presigner.presignPutObject(any(PutObjectPresignRequest.class))) - .thenReturn(presignedPutObjectRequest); - - ImageUploadUrlResponse response = imageService.createUploadUrl(userId, request); - - assertThat(response.objectKey()) - .startsWith("users/1/docs/2/thumbnails/") - .endsWith(".webp"); - - ArgumentCaptor presignCaptor = - ArgumentCaptor.forClass(PutObjectPresignRequest.class); - verify(s3Presigner).presignPutObject(presignCaptor.capture()); - - PutObjectRequest putObjectRequest = presignCaptor.getValue().putObjectRequest(); - assertThat(putObjectRequest.key()).isEqualTo(response.objectKey()); - assertThat(putObjectRequest.contentType()).isEqualTo("image/webp"); - } - @Test @DisplayName("지원하지 않는 이미지 형식이면 업로드 URL을 생성하지 않는다") void createUploadUrl_fail_whenContentTypeInvalid() { Long userId = 1L; Long docId = 2L; ImageUploadUrlRequest request = - new ImageUploadUrlRequest(docId, "sample.svg", "image/svg+xml", 1024L, Purpose.DOC_CONTENT); + new ImageUploadUrlRequest(docId, "sample.svg", "image/svg+xml", 1024L); when(docQueryService.getByIdAndUserId(docId, userId)).thenReturn(org.mockito.Mockito.mock(Doc.class)); @@ -177,10 +139,9 @@ void complete_success() { .objectKey(objectKey) .contentType("image/png") .size(1024L) - .purpose(Purpose.DOC_CONTENT) .build(); - when(imageQueryService.getByIdAndUserId(imageId, userId)).thenReturn(image); + when(imageRepository.findByIdAndUserId(imageId, userId)).thenReturn(Optional.of(image)); when(s3Client.headObject(any(HeadObjectRequest.class))).thenReturn( HeadObjectResponse.builder() .contentType("image/png") @@ -216,10 +177,9 @@ void complete_fail_whenUploadNotCompleted() { .objectKey("users/1/docs/2/images/image.png") .contentType("image/png") .size(1024L) - .purpose(Purpose.DOC_CONTENT) .build(); - when(imageQueryService.getByIdAndUserId(imageId, userId)).thenReturn(image); + when(imageRepository.findByIdAndUserId(imageId, userId)).thenReturn(Optional.of(image)); when(s3Client.headObject(any(HeadObjectRequest.class))).thenThrow( S3Exception.builder() .statusCode(404) diff --git a/src/test/java/io/ejangs/docsa/domain/save/api/SaveControllerTest.java b/src/test/java/io/ejangs/docsa/domain/save/api/SaveControllerTest.java index 13d61f07..da2666f7 100644 --- a/src/test/java/io/ejangs/docsa/domain/save/api/SaveControllerTest.java +++ b/src/test/java/io/ejangs/docsa/domain/save/api/SaveControllerTest.java @@ -9,8 +9,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailSyncResponse; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; import io.ejangs.docsa.domain.save.app.SaveService; import io.ejangs.docsa.domain.save.dto.SaveIdentifierDto; import io.ejangs.docsa.domain.save.dto.request.SaveUpdateRequest; @@ -68,7 +66,7 @@ void updateSave_success() throws Exception { Long saveId = 1L; when(saveService.updateSave(dto, request)).thenReturn(new SaveUpdateResponse( - LocalDateTime.now(), new ThumbnailSyncResponse(10L, "wow", ThumbnailStatus.READY))); + LocalDateTime.now())); mockMvc.perform( put("/api/document/{documentId}/save/{saveId}", documentId, saveId) diff --git a/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceIntegrationTest.java b/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceIntegrationTest.java index 89608576..2eb815e8 100644 --- a/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceIntegrationTest.java +++ b/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceIntegrationTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import com.mongodb.DuplicateKeyException; import io.ejangs.docsa.domain.branch.dao.mysql.BranchRepository; import io.ejangs.docsa.domain.branch.entity.Branch; import io.ejangs.docsa.domain.commit.dao.mysql.CommitRepository; @@ -37,7 +38,6 @@ import org.springframework.dao.RecoverableDataAccessException; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @SpringBootTest @@ -111,13 +111,10 @@ class SaveRollbackTest { @Test @DisplayName("Mongo 저장 실패 시 updateSave 롤백") - @Transactional(propagation = Propagation.NOT_SUPPORTED) + //@Transactional(propagation = Propagation.NOT_SUPPORTED) void updateSave_fails_whenMongoSaveFails_thenMysqlDeleted() throws Exception { // given - saveRepository.flush(); - em.clear(); - Save before = saveRepository.findById(save.getId()).orElseThrow(); - LocalDateTime beforeUpdatedAt = before.getUpdatedAt(); + LocalDateTime beforeUpdatedAt = save.getUpdatedAt(); SaveIdentifierDto dto = new SaveIdentifierDto(doc.getId(), save.getId(), user.getId()); SaveUpdateRequest request = new SaveUpdateRequest(data); diff --git a/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceUnitTest.java b/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceUnitTest.java index ae6c252f..b222bbd6 100644 --- a/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceUnitTest.java +++ b/src/test/java/io/ejangs/docsa/domain/save/app/SaveServiceUnitTest.java @@ -9,9 +9,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.ejangs.docsa.domain.doc.thumbnail.app.ThumbnailService; -import io.ejangs.docsa.domain.doc.thumbnail.dto.ThumbnailSyncResponse; -import io.ejangs.docsa.domain.doc.thumbnail.entity.Thumbnail.ThumbnailStatus; import io.ejangs.docsa.domain.save.document.SaveContent; import io.ejangs.docsa.domain.save.dto.SaveIdentifierDto; import io.ejangs.docsa.domain.save.dto.request.SaveUpdateRequest; @@ -40,8 +37,6 @@ class SaveServiceUnitTest { @Mock private SaveQueryService saveQueryService; @Mock - private ThumbnailService thumbnailService; - @Mock private Save mockSave; @Mock private SaveContent mockSaveContent; @@ -113,10 +108,7 @@ void getSave_fail_invalidSave() { @Test @DisplayName("성공적인 updateSave") void updateSave_success() { - ThumbnailSyncResponse thumbnailSyncResponse = new ThumbnailSyncResponse(10L, "wow", - ThumbnailStatus.READY); - SaveUpdateResponse expectedResponse = new SaveUpdateResponse(LocalDateTime.now(), - thumbnailSyncResponse); + SaveUpdateResponse expectedResponse = new SaveUpdateResponse(LocalDateTime.now()); when(saveQueryService.getSaveById(idDto.saveId())).thenReturn(mockSave); doNothing().when(saveQueryService) @@ -124,12 +116,9 @@ void updateSave_success() { when(mockSave.getSaveMongoId()).thenReturn("mongo-1"); when(saveQueryService.getSaveContentById("mongo-1")).thenReturn(mockSaveContent); when(mockSave.getUpdatedAt()).thenReturn(LocalDateTime.now()); - when(thumbnailService.requestUpdate(idDto.userId(), idDto.documentId())) - .thenReturn(thumbnailSyncResponse); try (MockedStatic mockedMapper = mockStatic(SaveMapper.class)) { - mockedMapper.when(() -> SaveMapper.toSaveUpdateResponse(mockSave.getUpdatedAt(), - thumbnailSyncResponse)) + mockedMapper.when(() -> SaveMapper.toSaveUpdateResponse(mockSave.getUpdatedAt())) .thenReturn(expectedResponse); SaveUpdateResponse actualResponse = saveService.updateSave(idDto, request); diff --git a/src/test/java/io/ejangs/docsa/global/outbox/mongo/MongoTransactionTestService.java b/src/test/java/io/ejangs/docsa/global/mongo/outbox/MongoTransactionTestService.java similarity index 96% rename from src/test/java/io/ejangs/docsa/global/outbox/mongo/MongoTransactionTestService.java rename to src/test/java/io/ejangs/docsa/global/mongo/outbox/MongoTransactionTestService.java index 2e292e0a..28eed5e9 100644 --- a/src/test/java/io/ejangs/docsa/global/outbox/mongo/MongoTransactionTestService.java +++ b/src/test/java/io/ejangs/docsa/global/mongo/outbox/MongoTransactionTestService.java @@ -1,4 +1,4 @@ -package io.ejangs.docsa.global.outbox.mongo; +package io.ejangs.docsa.global.mongo.outbox; import io.ejangs.docsa.domain.commit.dao.mysql.CommitRepository; import io.ejangs.docsa.domain.commit.entity.Commit; diff --git a/src/test/java/io/ejangs/docsa/global/outbox/mongo/MongoTransactionTests.java b/src/test/java/io/ejangs/docsa/global/mongo/outbox/MongoTransactionTests.java similarity index 97% rename from src/test/java/io/ejangs/docsa/global/outbox/mongo/MongoTransactionTests.java rename to src/test/java/io/ejangs/docsa/global/mongo/outbox/MongoTransactionTests.java index f9482777..b819a209 100644 --- a/src/test/java/io/ejangs/docsa/global/outbox/mongo/MongoTransactionTests.java +++ b/src/test/java/io/ejangs/docsa/global/mongo/outbox/MongoTransactionTests.java @@ -1,4 +1,4 @@ -package io.ejangs.docsa.global.outbox.mongo; +package io.ejangs.docsa.global.mongo.outbox; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; diff --git a/src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxFactoryUnitTest.java b/src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxFactoryUnitTest.java similarity index 87% rename from src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxFactoryUnitTest.java rename to src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxFactoryUnitTest.java index b8c05f94..0e84e65f 100644 --- a/src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxFactoryUnitTest.java +++ b/src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxFactoryUnitTest.java @@ -1,4 +1,4 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -7,12 +7,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxIntegrationTest.java b/src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxIntegrationTest.java similarity index 91% rename from src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxIntegrationTest.java rename to src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxIntegrationTest.java index a8bd4b0b..8df520ec 100644 --- a/src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxIntegrationTest.java +++ b/src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxIntegrationTest.java @@ -1,16 +1,16 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; -import io.ejangs.docsa.global.outbox.OutboxStatus; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OutboxStatus; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -48,8 +48,7 @@ class MongoDeleteOutboxIntegrationTest { @BeforeEach void cleanOutbox() { - mongoDeleteOutboxRepository.deleteAllInBatch(); - mongoDeleteOutboxRepository.flush(); + mongoDeleteOutboxRepository.deleteAll(); } @Test @@ -121,7 +120,7 @@ void factoryDedupeWithSameKey() { } @Test - @DisplayName("PROCESSING timeout 건은 retryCount 증가 없이 복구되어 재처리된다") + @DisplayName("PROCESSING timeout 건은 run() 시작 시 복구되어 재처리된다") void workerRecoversTimedOutProcessingBeforeDelete() { MongoDeleteOutbox outbox = createOpenOutbox(); mongoDeleteOutboxLifecycleService.claimOpen(outbox.getId()); @@ -139,7 +138,7 @@ void workerRecoversTimedOutProcessingBeforeDelete() { MongoDeleteOutbox done = mongoDeleteOutboxRepository.findById(outbox.getId()).orElseThrow(); assertThat(done.getStatus()).isEqualTo(OutboxStatus.DONE); - assertThat(done.getRetryCount()).isEqualTo(0); + assertThat(done.getRetryCount()).isEqualTo(1); } @Test diff --git a/src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxRaceIntegrationTest.java b/src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxRaceIntegrationTest.java similarity index 90% rename from src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxRaceIntegrationTest.java rename to src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxRaceIntegrationTest.java index 828a5b41..a2b8593c 100644 --- a/src/test/java/io/ejangs/docsa/global/outbox/mongo/app/MongoDeleteOutboxRaceIntegrationTest.java +++ b/src/test/java/io/ejangs/docsa/global/mongo/outbox/app/MongoDeleteOutboxRaceIntegrationTest.java @@ -1,13 +1,13 @@ -package io.ejangs.docsa.global.outbox.mongo.app; +package io.ejangs.docsa.global.mongo.outbox.app; import static org.assertj.core.api.Assertions.assertThat; -import io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository; -import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType; -import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType; +import io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository; +import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType; +import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.TriggerType; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -119,7 +119,7 @@ static void clear() { RaceBarrierAspect.barrier = null; } - @Around("execution(* io.ejangs.docsa.global.outbox.mongo.dao.mysql.MongoDeleteOutboxRepository.findByTriggerTypeAndDomainTypeAndOriginTypeAndOriginId(..))") + @Around("execution(* io.ejangs.docsa.global.mongo.outbox.dao.mysql.MongoDeleteOutboxRepository.findByTriggerTypeAndDomainTypeAndOriginTypeAndOriginId(..))") Object awaitAfterEmptyLookup(ProceedingJoinPoint joinPoint) throws Throwable { @SuppressWarnings("unchecked") Optional result = (Optional) joinPoint.proceed(); diff --git a/src/test/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteServiceUnitTest.java b/src/test/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteServiceUnitTest.java deleted file mode 100644 index fb603666..00000000 --- a/src/test/java/io/ejangs/docsa/global/outbox/s3/app/S3DeleteServiceUnitTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.ejangs.docsa.global.outbox.s3.app; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.ejangs.docsa.global.outbox.s3.dto.S3DeleteTarget; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Exception; - -@ExtendWith(MockitoExtension.class) -class S3DeleteServiceUnitTest { - - @Mock - private S3Client s3Client; - - private S3DeleteService s3DeleteService; - - @BeforeEach - void setUp() { - s3DeleteService = new S3DeleteService(s3Client); - ReflectionTestUtils.setField(s3DeleteService, "bucket", "docsa-image-bucket"); - } - - @Test - @DisplayName("S3 삭제 대상 objectKey로 DeleteObject를 호출한다") - void deleteTarget_success() { - S3DeleteTarget target = new S3DeleteTarget(10L, "users/1/docs/2/images/old.webp"); - when(s3Client.deleteObject(any(DeleteObjectRequest.class))) - .thenReturn(DeleteObjectResponse.builder().build()); - - s3DeleteService.deleteTarget(target); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(DeleteObjectRequest.class); - verify(s3Client).deleteObject(captor.capture()); - DeleteObjectRequest request = captor.getValue(); - assertThat(request.bucket()).isEqualTo("docsa-image-bucket"); - assertThat(request.key()).isEqualTo(target.objectKey()); - } - - @Test - @DisplayName("이미 삭제된 S3 객체는 성공으로 처리한다") - void deleteTarget_success_whenObjectMissing() { - S3DeleteTarget target = new S3DeleteTarget(10L, "users/1/docs/2/images/missing.webp"); - when(s3Client.deleteObject(any(DeleteObjectRequest.class))) - .thenThrow(S3Exception.builder().statusCode(404).build()); - - s3DeleteService.deleteTarget(target); - } - - @Test - @DisplayName("S3 삭제 중 404가 아닌 예외는 재시도를 위해 전파한다") - void deleteTarget_fail_whenS3ErrorOccurs() { - S3DeleteTarget target = new S3DeleteTarget(10L, "users/1/docs/2/images/old.webp"); - RuntimeException s3Exception = S3Exception.builder().statusCode(500).build(); - when(s3Client.deleteObject(any(DeleteObjectRequest.class))).thenThrow(s3Exception); - - assertThatThrownBy(() -> s3DeleteService.deleteTarget(target)) - .isSameAs(s3Exception); - } -} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 04c2b1c6..1bf305dd 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -9,9 +9,6 @@ spring: mongodb: uri: ${MONGO_URI:mongodb://localhost:27017/docsa-test} - flyway: - enabled: false - jpa: hibernate: ddl-auto: create @@ -63,14 +60,6 @@ mongo: fixed-delay: PT1M initial-delay: PT24H -s3: - delete: - outbox: - worker: - fixed-delay: PT1M - initial-delay: PT24H - - cloud: aws: region: ap-northeast-2