Skip to content

Commit 71fa30f

Browse files
authored
Merge: 생성 로직 보상트랜잭션 도입 및 서비스 분리
Refactor: 생성 로직 보상트랜잭션 도입 및 서비스 분리
2 parents 60e6974 + 73b9a2d commit 71fa30f

91 files changed

Lines changed: 2600 additions & 1519 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

infra/deploy.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ echo "[deploy] compose=$COMPOSE_FILE env=$ENV_FILE service=$SERVICE"
5757
docker compose "${ARGS[@]}" config >/dev/null
5858

5959
# 2) 이미지 풀 + 대상 서비스만 업데이트
60-
docker compose "${ARGS[@]}" pull "$SERVICE" || true
60+
docker compose "${ARGS[@]}" pull "$SERVICE"
6161
docker compose "${ARGS[@]}" up -d "$SERVICE"
6262

6363
# 3) 컨테이너 ID를 compose로 조회(이름 하드코딩 회피)
@@ -85,4 +85,4 @@ if [[ $ok -eq 0 ]]; then
8585
echo -e "\n$SERVICE failed to become healthy (status=${status:-unknown})"
8686
docker logs --tail=200 "$CID" || true
8787
exit 1
88-
fi
88+
fi

infra/docker-compose.yml

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ services:
104104
--agree-tos --non-interactive
105105
--no-eff-email
106106
107-
# 자동 갱신 데몬(하루마다 확인, 갱신되면 nginx 리로드)
107+
# 인증서 갱신용 컨테이너(cron)
108108
certbot_renew:
109109
image: certbot/certbot:latest
110110
container_name: docsa-certbot-renew
@@ -114,33 +114,6 @@ services:
114114
- ./certbot/www:/var/www/certbot:rw
115115
- ./certbot/etc:/etc/letsencrypt:rw
116116
- ./certbot/logs:/var/log/letsencrypt:rw
117-
- /var/run/docker.sock:/var/run/docker.sock:rw
118-
entrypoint: >
119-
sh -c '
120-
set -eu
121-
122-
# curl 없으면 설치
123-
if ! command -v curl >/dev/null 2>&1; then
124-
if command -v apk >/dev/null 2>&1; then
125-
apk add --no-cache curl >/dev/null 2>&1
126-
else
127-
echo "[ERROR] curl not found and apk not available. Need curl-capable image." >&2
128-
exit 1
129-
fi
130-
fi
131-
132-
while :; do
133-
# renew 실행
134-
certbot renew --webroot -w /var/www/certbot --quiet || true
135-
136-
# nginx 무중단 reload (Docker Engine API)
137-
curl -sS --unix-socket /var/run/docker.sock \
138-
-X POST "http://localhost/v1.41/containers/docsa-nginx/kill?signal=HUP" \
139-
>/dev/null 2>&1 || true
140-
141-
sleep 24h
142-
done
143-
'
144117

145118
# ===== METRICS =====
146119
cadvisor:
@@ -228,4 +201,4 @@ volumes:
228201
grafana_data:
229202

230203
networks:
231-
docsa_net:
204+
docsa_net:

infra/scripts/cert_renew.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5+
cd "$ROOT"
6+
7+
# Slack webhook (선택)
8+
ENV_FILE="$ROOT/docsa-alert.env"
9+
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
10+
11+
notify_slack () {
12+
local msg="$1"
13+
[[ -z "${SLACK_WEBHOOK_URL:-}" ]] && return 0
14+
curl -sS -X POST -H 'Content-type: application/json' \
15+
--data "{\"text\":${msg@Q}}" \
16+
"$SLACK_WEBHOOK_URL" >/dev/null 2>&1 || true
17+
}
18+
19+
# ---- 1) certbot renew 실행 ----
20+
out="$(
21+
docker compose run --rm --no-deps certbot_renew \
22+
certbot renew --webroot -w /var/www/certbot 2>&1 || true
23+
)"
24+
25+
echo "$out"
26+
27+
# ---- 2) 실패 감지 ----
28+
# certbot이 실패할 때 자주 나오는 문구 위주로 (오탐 줄임)
29+
if echo "$out" | grep -Eqi \
30+
"certbot failed to authenticate|failed authorization procedure|the following errors were reported|unauthorized|too many requests|nxdomain|connection refused|timeout"; then
31+
err_summary="$(echo "$out" | tail -n 40)"
32+
notify_slack "❌ *Certbot renew 실패*
33+
\`\`\`
34+
${err_summary}
35+
\`\`\`"
36+
exit 1
37+
fi
38+
39+
# ---- 3) 갱신됨 감지 (갱신 성공 시에만 nginx reload) ----
40+
# 성공 시 출력되는 대표 문구들
41+
renewed=0
42+
if echo "$out" | grep -Eqi "successfully renewed|congratulations, all renewals succeeded|renewal succeeded"; then
43+
renewed=1
44+
fi
45+
46+
if (( renewed == 1 )); then
47+
# 실행 중인 컨테이너만 HUP
48+
for c in docsa-nginx docsa-nginx-stg; do
49+
if [[ -n "$(docker ps -q -f "name=^/${c}$")" ]]; then
50+
docker kill -s HUP "$c" >/dev/null 2>&1 || true
51+
fi
52+
done
53+
54+
# ---- 4) 실제 서비스에서 인증서 만료일 확인(nginx 적용 검증) ----
55+
cert_api="$(echo | openssl s_client -connect api.docsa.o-r.kr:443 -servername api.docsa.o-r.kr 2>/dev/null \
56+
| openssl x509 -noout -enddate | cut -d= -f2 || true)"
57+
58+
cert_stg="$(echo | openssl s_client -connect stg.api.docsa.o-r.kr:8443 -servername stg.api.docsa.o-r.kr 2>/dev/null \
59+
| openssl x509 -noout -enddate | cut -d= -f2 || true)"
60+
61+
notify_slack "✅ *인증서 갱신 성공* → Nginx Reload 완료
62+
• api 만료일: \`${cert_api:-확인실패}\`
63+
• stg 만료일: \`${cert_stg:-확인실패}\`"
64+
else
65+
# 갱신 불필요도 정상. (스팸 방지로 슬랙은 안 보냄)
66+
echo "[info] No renewal needed."
67+
fi

infra/scripts/check_cert_expiry.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ENV_FILE="/srv/docsa/infra/docsa-alert.env"
5+
THRESHOLD_DAYS=14
6+
7+
DOMAINS=(
8+
"api.docsa.o-r.kr:443"
9+
"stg.api.docsa.o-r.kr:8443"
10+
)
11+
12+
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
13+
14+
notify_slack () {
15+
local msg="$1"
16+
[[ -z "${SLACK_WEBHOOK_URL:-}" ]] && return 0
17+
curl -sS -X POST -H 'Content-type: application/json' \
18+
--data "{\"text\":${msg@Q}}" \
19+
"$SLACK_WEBHOOK_URL" >/dev/null || true
20+
}
21+
22+
failed=0
23+
now_epoch=$(date -u +%s)
24+
25+
for target in "${DOMAINS[@]}"; do
26+
host="${target%%:*}"
27+
port="${target##*:}"
28+
29+
exp_date=$(echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null \
30+
| openssl x509 -noout -enddate \
31+
| cut -d= -f2 || true)
32+
33+
[[ -z "$exp_date" ]] && {
34+
notify_slack "$host:$port 인증서 만료일 조회 실패"
35+
failed=1
36+
continue
37+
}
38+
39+
exp_epoch=$(date -u -d "$exp_date" +%s)
40+
remain_days=$(( (exp_epoch - now_epoch) / 86400 ))
41+
42+
if (( remain_days <= THRESHOLD_DAYS )); then
43+
notify_slack "⚠️ $host:$port 인증서 만료 임박 D-${remain_days} (만료: $exp_date UTC)"
44+
failed=1
45+
fi
46+
done
47+
48+
exit "$failed"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ENV_FILE="/srv/docsa/infra/docsa-alert.env"
5+
TIMEOUT=5
6+
7+
TARGETS=(
8+
"api.docsa.o-r.kr:443"
9+
"stg.api.docsa.o-r.kr:8443"
10+
)
11+
12+
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
13+
14+
notify_slack () {
15+
local msg="$1"
16+
[[ -z "${SLACK_WEBHOOK_URL:-}" ]] && return 0
17+
curl -sS -X POST -H 'Content-type: application/json' \
18+
--data "{\"text\":${msg@Q}}" \
19+
"$SLACK_WEBHOOK_URL" >/dev/null 2>&1 || true
20+
}
21+
22+
for target in "${TARGETS[@]}"; do
23+
host="${target%%:*}"
24+
port="${target##*:}"
25+
26+
if ! timeout "${TIMEOUT}" bash -c "</dev/tcp/${host}/${port}" 2>/dev/null; then
27+
notify_slack "🚨 아아 공습경보 서버 불량: ${host}:${port}"
28+
fi
29+
done
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.ejangs.docsa.domain.branch.app;
2+
3+
import io.ejangs.docsa.domain.branch.dao.mysql.BranchRepository;
4+
import io.ejangs.docsa.domain.branch.entity.Branch;
5+
import io.ejangs.docsa.domain.commit.entity.Commit;
6+
import io.ejangs.docsa.domain.edge.dto.graph.BranchGraphDto;
7+
import io.ejangs.docsa.domain.doc.entity.Doc;
8+
import io.ejangs.docsa.global.exception.CustomException;
9+
import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode;
10+
import io.ejangs.docsa.global.util.RenewUpdatedAtHelper;
11+
import java.util.List;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
public class BranchQueryService {
19+
20+
private final BranchRepository branchRepository;
21+
22+
@Transactional(readOnly = true)
23+
public Branch getById(Long id) {
24+
return branchRepository.findById(id).orElseThrow(() -> new CustomException(BranchErrorCode.BRANCH_NOT_FOUND));
25+
}
26+
27+
@Transactional(readOnly = true)
28+
public List<BranchGraphDto> getBranchGraphList(Long docId) {
29+
return branchRepository.findBranchGraphDtoList(docId);
30+
}
31+
32+
public Branch save(Branch branch) {
33+
return branchRepository.save(branch);
34+
}
35+
36+
public Branch createBranch(Doc doc, String branchName) {
37+
Branch branch = branchRepository.save(Branch.builder().name(branchName).doc(doc).build());
38+
RenewUpdatedAtHelper.touch(branch);
39+
return branch;
40+
}
41+
42+
public Branch createBranch(Doc doc, String branchName, Commit fromCommit) {
43+
Branch branch = Branch.builder().name(branchName).doc(doc).fromCommit(fromCommit).build();
44+
return branchRepository.save(branch);
45+
}
46+
47+
@Transactional(readOnly = true)
48+
public boolean existsSubBranchByFromCommitIds(List<Long> commitIds) {
49+
return branchRepository.existsByFromCommitIdIn(commitIds);
50+
}
51+
52+
public void delete(Branch branch) {
53+
branchRepository.delete(branch);
54+
}
55+
56+
//TODO: 검증하는 메소드는 별도의 ex.ValidationService로 분리(모든 도메인)
57+
public void checkBranchInDocOwnedByUser(Long documentId, Long branchId, Long userId) {
58+
boolean exists =
59+
branchRepository.existsByIdAndDocIdAndDocUserId(branchId, documentId, userId);
60+
if (!exists) {
61+
throw new CustomException(BranchErrorCode.BRANCH_NOT_FOUND);
62+
}
63+
}
64+
65+
public boolean checkFromOrRootCommitInBranch(Commit commit) {
66+
return branchRepository.existsByRootCommitIdOrFromCommitId(commit.getId());
67+
}
68+
69+
public void checkDuplicatedWithBranchName(Long docId, String branchName) {
70+
boolean isDuplicate = branchRepository.existsByDocIdAndName(docId, branchName);
71+
72+
if (isDuplicate) {
73+
throw new CustomException(BranchErrorCode.BRANCH_NAME_DUPLICATED);
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)