Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fcd8d66
refactor: 문서 목록 최신 저장 조회를 배치화
lunarbae628 Apr 30, 2026
e9a186d
refactor: EntityGraph로 thumbnail, thumbnail.currentImage 조회
lunarbae628 May 1, 2026
004f770
refactor: Mongo 삭제 outbox originType 삭제 및 MongoDeleteOutboxFactory의 유…
lunarbae628 May 1, 2026
18bd523
refactor: Mongo/S3 삭제 Outbox 관련 클래스명을 역할에 맞게 정리
lunarbae628 May 2, 2026
ddca378
feat: 문서 목록 CQRS 기반을 위한 도메인 이벤트 Outbox 추가
lunarbae628 May 4, 2026
db46aec
feat: 문서 목록 read model 이벤트 payload 분리
lunarbae628 May 7, 2026
8cb0867
feat: 문서 생성과 제목 변경에 문서 목록 이벤트 발행 연결
lunarbae628 May 7, 2026
c0b32bf
fix: 인프라 인증서 갱신 스크립트 Slack payload 생성 방식 수정
lunarbae628 May 7, 2026
5ccea77
fix: 사용하지 않는 PreviewExtractor 제거
lunarbae628 May 7, 2026
23cc618
chore: eventoutbox 주기 수정(application.yml)
lunarbae628 May 7, 2026
869306c
chore: 도메인 이벤트 outbox wake-up 전용 Async executor 추가 및 미사용 import 제거
lunarbae628 May 7, 2026
95a2506
feat: 문서 목록 read model 갱신 이벤트 발행 연결
lunarbae628 May 8, 2026
2f22942
test: 문서 목록 read model projection 검증 보강
lunarbae628 May 8, 2026
a555f64
chore: 미사용 import 정리
lunarbae628 May 8, 2026
a0d5fd0
feat: 문서 목록 조회를 read model 기반 QueryService로 전환
lunarbae628 May 10, 2026
cad4620
chore: 테스트 출력 및 환경별 로그 설정 정리
lunarbae628 May 10, 2026
2b6568b
chore: 테스트 passed 로그 제거
lunarbae628 May 10, 2026
d8c8057
fix: local 로그 설정 boolean 바인딩 오류 수정
lunarbae628 May 10, 2026
072fd82
test: 문서 QueryService 테스트 패키지 정리 및 썸네일 URL 검증 추가
lunarbae628 May 11, 2026
85c7d0e
feat: local/test Mongo cleanup 추가
lunarbae628 May 11, 2026
dad62b3
refactor: CommitQueryService 책임 분리
lunarbae628 May 11, 2026
506fc02
refactor: BranchQueryService 책임 분리
lunarbae628 May 11, 2026
2a02f97
refactor: SaveQueryService 책임 분리
lunarbae628 May 11, 2026
993d593
refactor: 이미지와 썸네일 QueryService 명명 정리
lunarbae628 May 11, 2026
f3016a8
feat: 문서 목록 read model backfill job 추가
lunarbae628 May 12, 2026
2069b20
refactor: 문서 목록 backfill 패키지 분리
lunarbae628 May 12, 2026
afdbedb
chore: backfill 실행 설정 정리
lunarbae628 May 13, 2026
0dfd1a8
test: 문서 목록 read model 삭제 제외 조회 검증
lunarbae628 May 13, 2026
965667c
Merge: 문서 목록 CQRS Read Model 전환 및 Domain Event Outbox 추가
lunarbae628 May 14, 2026
7fffcc1
Fix: 환경변수 이름과 compose 주입 방식 정리
lunarbae628 May 14, 2026
c064421
Fix: 백필 스크립트를 Docker Compose 기반으로 변경
lunarbae628 May 15, 2026
41819f4
Chore: docker-compose.local.yml 추가 - MongoDB 로컬 도커 구동
lunarbae628 May 16, 2026
e4c40bb
chore: DocListReadModel에 조회/정렬 조건에 맞춘 compound index 추가
lunarbae628 May 16, 2026
1956c36
chore: cleanup_perf_docs.sh 파싱 오류 수정
lunarbae628 May 16, 2026
23f15e4
fix: 문서 생성 유니크 제약 충돌을 도메인 오류로 처리
lunarbae628 May 19, 2026
a3d0b4b
feat: 로컬 이미지 업로드를 MinIO로 분리
lunarbae628 May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ repositories {

test {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
events "skipped", "failed"
showStandardStreams = false
exceptionFormat "full"
showCauses true
showExceptions true
Expand Down
116 changes: 116 additions & 0 deletions infra/docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: docsa-local

services:
mysql:
image: mysql:9.4.0
container_name: docsa-mysql-local
restart: unless-stopped
environment:
MYSQL_DATABASE: ${LOCAL_MYSQL_DATABASE:-docsa}
MYSQL_USER: ${LOCAL_MYSQL_USER:-docsa}
MYSQL_PASSWORD: ${LOCAL_MYSQL_PASSWORD:-docsa}
MYSQL_ROOT_PASSWORD: ${LOCAL_MYSQL_ROOT_PASSWORD:-root}
TZ: Asia/Seoul
ports:
- "${LOCAL_MYSQL_PORT:-3306}:3306"
volumes:
- local_mysql_data:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- docsa_local_net

mongo:
image: mongo:8.0
container_name: docsa-mongo-local
restart: unless-stopped
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "${LOCAL_MONGO_PORT:-27017}:27017"
volumes:
- local_mongo_data:/data/db
healthcheck:
test:
[
"CMD-SHELL",
"mongosh --quiet --eval 'try { rs.status().ok } catch (e) { rs.initiate({_id:\"rs0\",members:[{_id:0,host:\"localhost:27017\"}]}); 1 }' | grep 1"
]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
networks:
- docsa_local_net

mailpit:
image: axllent/mailpit:latest
container_name: docsa-mailpit-local
restart: unless-stopped
environment:
MP_SMTP_AUTH_ACCEPT_ANY: "1"
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
MP_DATABASE: /data/mailpit.db
MP_MAX_MESSAGES: "5000"
ports:
- "${LOCAL_MAILPIT_SMTP_PORT:-1025}:1025"
- "${LOCAL_MAILPIT_WEB_PORT:-8025}:8025"
volumes:
- local_mailpit_data:/data
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8025/readyz || exit 1"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
networks:
- docsa_local_net

minio:
image: minio/minio:latest
container_name: docsa-minio-local
restart: unless-stopped
command: ["server", "/data", "--console-address", ":9001"]
environment:
MINIO_ROOT_USER: ${LOCAL_MINIO_ROOT_USER:-docsa-local}
MINIO_ROOT_PASSWORD: ${LOCAL_MINIO_ROOT_PASSWORD:-docsa-local-password}
MINIO_API_CORS_ALLOW_ORIGIN: ${LOCAL_MINIO_CORS_ALLOW_ORIGIN:-http://localhost:3000,http://localhost:5173}
TZ: Asia/Seoul
ports:
- "${LOCAL_MINIO_API_PORT:-9000}:9000"
- "${LOCAL_MINIO_CONSOLE_PORT:-9001}:9001"
volumes:
- local_minio_data:/data
networks:
- docsa_local_net

minio-init:
image: minio/mc:latest
container_name: docsa-minio-init-local
depends_on:
- minio
environment:
LOCAL_MINIO_ROOT_USER: ${LOCAL_MINIO_ROOT_USER:-docsa-local}
LOCAL_MINIO_ROOT_PASSWORD: ${LOCAL_MINIO_ROOT_PASSWORD:-docsa-local-password}
LOCAL_MINIO_BUCKET: ${LOCAL_MINIO_BUCKET:-docsa-local}
entrypoint: >
/bin/sh -c "
until mc alias set local http://minio:9000 ${LOCAL_MINIO_ROOT_USER:-docsa-local} ${LOCAL_MINIO_ROOT_PASSWORD:-docsa-local-password}; do sleep 2; done;
mc mb --ignore-existing local/${LOCAL_MINIO_BUCKET:-docsa-local};
mc anonymous set download local/${LOCAL_MINIO_BUCKET:-docsa-local};
"
networks:
- docsa_local_net

volumes:
local_mysql_data:
local_mongo_data:
local_mailpit_data:
local_minio_data:

networks:
docsa_local_net:
11 changes: 0 additions & 11 deletions infra/docker-compose.stg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,6 @@ services:
container_name: docsa-app-stg
restart: unless-stopped
env_file: .stg.env
environment:
SPRING_DATA_MONGODB_URI: ${SPRING_DATA_MONGODB_URI}
SPRING_MAIL_USERNAME: ${SPRING_MAIL_USERNAME}
#SPRING_MAIL_PASSWORD: ${SPRING_MAIL_PASSWORD}
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
SERVER_SERVLET_SESSION_COOKIE_NAME: ${SESSION_COOKIE_NAME}
SERVER_SERVLET_SESSION_COOKIE_DOMAIN: ${SESSION_COOKIE_DOMAIN}
SPRINGDOC_SWAGGER-UI_SERVERS_URL: ${SWAGGER_SERVER_URL}
SPRING_JPA_HIBERNATE_DDL_AUTO: ${DDL_AUTO}
depends_on:
mysql:
condition: service_healthy
Expand Down
10 changes: 0 additions & 10 deletions infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@ services:
container_name: docsa-app
restart: unless-stopped
env_file: .env
environment:
SPRING_DATA_MONGODB_URI: ${SPRING_DATA_MONGODB_URI}
SPRING_MAIL_USERNAME: ${SPRING_MAIL_USERNAME}
SPRING_MAIL_PASSWORD: ${SPRING_MAIL_PASSWORD}
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
SERVER_SERVLET_SESSION_COOKIE_NAME: ${SESSION_COOKIE_NAME}
SERVER_SERVLET_SESSION_COOKIE_DOMAIN: ${SESSION_COOKIE_DOMAIN}
SPRINGDOC_SWAGGER-UI_SERVERS_URL: ${SWAGGER_SERVER_URL}
depends_on:
mysql:
condition: service_healthy
Expand Down
21 changes: 21 additions & 0 deletions infra/scripts/backfill-doc-list-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

COMPOSE_FILE="${BACKFILL_COMPOSE_FILE:-"$ROOT_DIR/infra/docker-compose.stg.yml"}"
SERVICE="${BACKFILL_SERVICE:-app}"
PROFILES="${BACKFILL_PROFILES:-stg,backfill}"
BATCH_SIZE="${DOC_LIST_READ_MODEL_BACKFILL_BATCH_SIZE:-1000}"
DDL_AUTO="${BACKFILL_DDL_AUTO:-validate}"

echo "[backfill] composeFile=$COMPOSE_FILE service=$SERVICE profiles=$PROFILES batchSize=$BATCH_SIZE ddlAuto=$DDL_AUTO"

docker compose -f "$COMPOSE_FILE" run --rm \
-e SPRING_PROFILES_ACTIVE="$PROFILES" \
-e SPRING_JPA_HIBERNATE_DDL_AUTO="$DDL_AUTO" \
-e DDL_AUTO="$DDL_AUTO" \
-e DOC_LIST_READ_MODEL_BACKFILL_BATCH_SIZE="$BATCH_SIZE" \
-e MONGO_LOCAL_CLEANUP_ENABLED=false \
-e PERF_SEED_USER_COUNT=0 \
"$SERVICE"
10 changes: 6 additions & 4 deletions infra/scripts/cert_renew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ ENV_FILE="$ROOT/docsa-alert.env"
notify_slack () {
local msg="$1"
[[ -z "${SLACK_WEBHOOK_URL:-}" ]] && return 0

local payload
payload="$(python3 -c 'import json, sys; print(json.dumps({"text": sys.argv[1]}))' "$msg")"

curl -sS -X POST -H 'Content-type: application/json' \
--data "{\"text\":${msg@Q}}" \
--data "$payload" \
"$SLACK_WEBHOOK_URL" >/dev/null 2>&1 || true
}

# ---- 1) certbot renew 실행 ----
out="$(
docker compose run --rm --no-deps certbot_renew \
certbot renew --webroot -w /var/www/certbot 2>&1 || true
renew --webroot -w /var/www/certbot 2>&1 || true
)"

echo "$out"

# ---- 2) 실패 감지 ----
# certbot이 실패할 때 자주 나오는 문구 위주로 (오탐 줄임)
if echo "$out" | grep -Eqi \
Expand Down
2 changes: 1 addition & 1 deletion perf/cleanup_perf_docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ login_cookie() {
cookie="$(
grep -i '^set-cookie:' "${headers}" \
| tr -d '\r' \
| sed -n "s/^set-cookie:[[:space:]]*${SESSION_COOKIE_NAME}=\\([^;]*\\).*/\\1/p" \
| sed -n "s/^[Ss][Ee][Tt]-[Cc][Oo][Oo][Kk][Ii][Ee]:[[:space:]]*${SESSION_COOKIE_NAME}=\\([^;]*\\).*/\\1/p" \
| head -n 1
)"
rm -f "${headers}" "${body}"
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/io/ejangs/docsa/DocsaApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@OpenAPIDefinition(
Expand All @@ -14,7 +13,6 @@
description = "Docsa의 백엔드 API 명세입니다."
)
)
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class DocsaApplication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@
import io.ejangs.docsa.domain.branch.entity.Branch;
import io.ejangs.docsa.domain.commit.entity.Commit;
import io.ejangs.docsa.domain.edge.dto.graph.BranchGraphDto;
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.util.RenewUpdatedAtHelper;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class BranchQueryService {
public class BranchReader {

private final BranchRepository branchRepository;

Expand All @@ -29,31 +27,11 @@ public List<BranchGraphDto> getBranchGraphList(Long docId) {
return branchRepository.findBranchGraphDtoList(docId);
}

public Branch save(Branch branch) {
return branchRepository.save(branch);
}

public Branch createBranch(Doc doc, String branchName) {
Branch branch = branchRepository.save(Branch.builder().name(branchName).doc(doc).build());
RenewUpdatedAtHelper.touch(branch);
return branch;
}

public Branch createBranch(Doc doc, String branchName, Commit fromCommit) {
Branch branch = Branch.builder().name(branchName).doc(doc).fromCommit(fromCommit).build();
return branchRepository.save(branch);
}

@Transactional(readOnly = true)
public boolean existsSubBranchByFromCommitIds(List<Long> commitIds) {
return branchRepository.existsByFromCommitIdIn(commitIds);
}

public void delete(Branch branch) {
branchRepository.delete(branch);
}

//TODO: 검증하는 메소드는 별도의 ex.ValidationService로 분리(모든 도메인)
public void checkBranchInDocOwnedByUser(Long documentId, Long branchId, Long userId) {
boolean exists =
branchRepository.existsByIdAndDocIdAndDocUserId(branchId, documentId, userId);
Expand Down
Loading
Loading