diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c92dda9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# 컨테이너 빌드 컨텍스트 축소 — 빌드 산출물/캐시/대용량 문서 제외. +.git +.gitignore +**/node_modules +**/build +**/.gradle +**/dist +frontend/dist +*.log +docs/*.pdf +docs/*.xlsx +docs/*.7z +docs/*.png +.claude +.vscode +sim +loadtest diff --git a/deploy/Dockerfile.backend b/deploy/Dockerfile.backend new file mode 100644 index 0000000..96a8a9d --- /dev/null +++ b/deploy/Dockerfile.backend @@ -0,0 +1,31 @@ +# RTGS 백엔드 공용 이미지 — 7개 서비스(sequencer/chanel/dior/hermes/prada/gucci/louisvuitton) 하나로. +# 실행 서비스는 SERVICE env로 선택. 포트/DB/센터/Kafka는 전부 env 주입(compose에서 지정). +# ── build: gradlew로 fat jar 빌드(JDK21). runtime: JRE + curl(헬스체크). + +# ---------- build ---------- +FROM eclipse-temurin:21-jdk AS build +WORKDIR /src +COPY backend/ ./ +# 오프라인 캐시락 회피 위해 --no-build-cache. 의존성은 최초 1회 네트워크 다운로드. +RUN chmod +x gradlew && ./gradlew --no-daemon --no-build-cache \ + :sequencer:bootJar :chanel:bootJar :dior:bootJar :hermes:bootJar \ + :prada:bootJar :gucci:bootJar :louisvuitton:bootJar + +# ---------- runtime ---------- +FROM eclipse-temurin:21-jre AS runtime +WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +# 7개 fat jar를 /app/.jar 로 복사 +COPY --from=build /src/sequencer/build/libs/sequencer-0.1.0.jar /app/sequencer.jar +COPY --from=build /src/chanel/build/libs/chanel-0.1.0.jar /app/chanel.jar +COPY --from=build /src/dior/build/libs/dior-0.1.0.jar /app/dior.jar +COPY --from=build /src/hermes/build/libs/hermes-0.1.0.jar /app/hermes.jar +COPY --from=build /src/prada/build/libs/prada-0.1.0.jar /app/prada.jar +COPY --from=build /src/gucci/build/libs/gucci-0.1.0.jar /app/gucci.jar +COPY --from=build /src/louisvuitton/build/libs/louisvuitton-0.1.0.jar /app/louisvuitton.jar + +# SERVICE=chanel 등으로 실행 서비스 선택. SERVER_PORT는 Spring relaxed-binding으로 server.port에 매핑. +ENV SERVICE=chanel +ENTRYPOINT ["sh","-c","exec java -jar /app/${SERVICE}.jar"] diff --git a/deploy/Dockerfile.frontend b/deploy/Dockerfile.frontend new file mode 100644 index 0000000..2877029 --- /dev/null +++ b/deploy/Dockerfile.frontend @@ -0,0 +1,17 @@ +# RTGS 프론트 이미지 — 콘솔(index.html) + DT(dt.html) 정적 빌드 → nginx 서빙 + API 리버스프록시. +# ---------- build ---------- +FROM node:22-bookworm-slim AS build +WORKDIR /app +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build # vite build → dist (index.html + dt.html 두 엔트리) + +# ---------- runtime ---------- +FROM nginx:1.27-alpine AS runtime +RUN apk add --no-cache curl +COPY --from=build /app/dist /usr/share/nginx/html +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +HEALTHCHECK --interval=10s --timeout=4s --retries=10 \ + CMD curl -fs http://localhost/ >/dev/null || exit 1 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..605cac7 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,48 @@ +# RTGS 컨테이너 배포 (deploy/) + +RTGS 3센터 A-A-A 풀스택을 컨테이너로 기동하는 산출물. **로컬 native 실행(`run-*.cmd`)과 별개**이며, Docker/podman 호스트(사내 **Coder** 워크스페이스 등)에서 사용한다. + +> ⚠️ 이 산출물은 **Coder(podman)에서 빌드·검증**한다. 개발 로컬 PC는 가상화 차단으로 Docker가 없어 여기서 build/up 불가 — 파일 정합성만 로컬 검증됨. + +## 구성 +| 파일 | 역할 | +|---|---| +| `Dockerfile.backend` | 7개 서비스 공용 이미지(멀티스테이지 gradle 빌드). `SERVICE` env로 실행 서비스 선택 | +| `Dockerfile.frontend` + `nginx.conf` | 콘솔+DT 정적 빌드 → nginx 서빙 + API 리버스프록시(센터 라우팅) | +| `docker-compose.yml` | PostgreSQL(3DB) + Kafka + 순번기 + DC1/DC2/DC3 각 6서비스 + 프론트 | +| `init/init-3centers.sql` | rtgs_dc1/2/3 DB 생성(스키마·시드는 각 센터 LouisVuitton Flyway) | + +포트 오프셋 없음(컨테이너 격리) — service name으로 구분. 센터 간 통신은 `CHANEL_URL=http://dcN-chanel:8091`, `KAFKA_BOOTSTRAP=kafka:9092`, `POSTGRES_HOST=postgres` 로 연결. + +## 기동 (Coder에서) +```bash +# 저장소 clone (Gitea 토큰 인증) 후 프로젝트 루트에서: +docker compose -f deploy/docker-compose.yml up --build -d +docker compose -f deploy/docker-compose.yml ps # 헬스 확인(전 서비스 healthy까지 1~2분) +``` +- 접속: 콘솔 `http://:8080/`, 단말(DT) `http://:8080/dt.html` +- 검증(로컬에서 통과한 기준 동일): + 1. DT에서 이체 → RCVD → ACCC + 2. 콘솔 상단 "모니터링 센터" DC1/DC2/DC3 전환 → **📈 보고서 완전 동일**(거래·금액·대사·기관별) + 3. 정족수: `dc3-hermes` 중지해도 ACCC(2/3) — `docker compose stop dc3-hermes` +- 직접 확인용 호스트 포트: LV `8099/8199/8299`, Gucci `8095/8195/8295` + +## Gitea push (Coder 내부) +```bash +git remote add origin https://gitea.bokdev.in/playground/rtgs.git # 서버 프로젝트명은 "POC", 저장소는 rtgs +git push -u origin main # 자격증명 프롬프트 시 Gitea 개인 액세스 토큰(repo read/write) +``` +> 로컬 PC 셸에서는 gitea.bokdev.in 미도달(사내망) — **push/배포는 Coder 안에서**. (ACS도 동일) + +## Coolify/Kubero 어댑테이션 (플랫폼 정책 확인 필요) +ACS는 "**단일 Node 컨테이너 + 플랫폼 관리형 PostgreSQL(자체 DB 컨테이너 금지)**" 정책이었다. RTGS는 Kafka + 다중 JVM + 3 DB라 그 틀과 다르므로 **인프라팀 확인 필수**: +- **Coolify가 docker-compose(다중 컨테이너) 리소스를 허용**하면 → 본 compose를 그대로 Public Repository + Compose로 등록. +- **관리형 PostgreSQL 강제** 시 → `postgres` 서비스 제거, 각 서비스 env를 플랫폼 제공 `DATABASE_URL`(또는 `POSTGRES_HOST/PORT/USER/PASSWORD`)로 교체. 단 3개 논리 DB(rtgs_dc1/2/3)가 필요하므로 관리형 인스턴스에 DB 3개 생성 권한 필요. +- **Kafka 미지원** 시 → 관리형 메시징 대체 또는 별도 배치 협의(저널 = 설계 핵심이라 제거 불가). +- 도메인: `https://rtgs.apps.bokdev.in`(포털 등록값) 또는 매뉴얼상 `*.apps.bokdev.in`. + +## 주의 (ACS 시행착오 반영) +- 헬스체크용 `curl`을 런타임 이미지에 포함(미포함 시 Coolify healthcheck 실패). +- 저장소는 `playground` 조직(개인 네임스페이스 금지). +- Kafka 저널은 **단일 파티션**(전역순서) — `KAFKA_NUM_PARTITIONS=1` 유지. +- PostgreSQL `max_connections=300`(19서비스 커넥션). 관리형 DB면 동등 이상 확인. diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..d2dbaf9 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,185 @@ +name: rtgs + +# RTGS 3센터 A-A-A 풀스택 (컨테이너). 로컬 native(run-*.cmd)와 별개 — Coder/podman 등 컨테이너 호스트용. +# 공유 인프라(PostgreSQL 3DB + Kafka) + 단일 순번기 + DC1/DC2/DC3 각 6서비스 + 프론트(콘솔·DT). +# 컨테이너는 네트워크 격리라 포트 오프셋 불필요(전부 기본 포트, service name으로 구분). + +# ---------- 공용 백엔드(서비스 공통) ---------- +x-backend-env: &backend-env + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_USER: rtgs + POSTGRES_PASSWORD: rtgs + KAFKA_BOOTSTRAP: kafka:9092 + CENTER_COUNT: "3" + GUCCI_JWT_SECRET: rtgs-gucci-dev-secret-key-please-change-0123456789 + +x-backend: &backend + image: rtgs-backend:latest + build: + context: .. + dockerfile: deploy/Dockerfile.backend + restart: unless-stopped + +x-health: &svc-health + healthcheck: + test: ["CMD-SHELL", "curl -fs http://localhost:$${SERVER_PORT}/actuator/health || exit 1"] + interval: 10s + timeout: 4s + retries: 30 + start_period: 40s + +services: + # ================= 인프라 ================= + postgres: + image: postgres:16 + container_name: rtgs-postgres + environment: + POSTGRES_DB: rtgs + POSTGRES_USER: rtgs + POSTGRES_PASSWORD: rtgs + command: ["postgres", "-c", "max_connections=300"] + volumes: + - pgdata:/var/lib/postgresql/data + - ./init/init-3centers.sql:/docker-entrypoint-initdb.d/01-init-3centers.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U rtgs -d rtgs"] + interval: 5s + timeout: 3s + retries: 30 + + kafka: + image: apache/kafka:3.8.1 + container_name: rtgs-kafka + environment: + KAFKA_NODE_ID: "1" + KAFKA_PROCESS_ROLES: "broker,controller" + KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" + KAFKA_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093" + KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092" + KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1" + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1" + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1" + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: "0" + KAFKA_NUM_PARTITIONS: "1" # 저널 전역순서 = 단일 파티션 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + healthcheck: + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 20s + + # ================= 순번기(전역 단일, DC1) ================= + sequencer: + <<: [*backend, *svc-health] + container_name: rtgs-sequencer + environment: {<<: *backend-env, SERVICE: sequencer, SERVER_PORT: "8090", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1} + depends_on: + kafka: {condition: service_healthy} + dc1-louisvuitton: {condition: service_healthy} + + # ================= DC1 ================= + dc1-louisvuitton: + <<: [*backend, *svc-health] + container_name: rtgs-dc1-louisvuitton + environment: {<<: *backend-env, SERVICE: louisvuitton, SERVER_PORT: "8099", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1} + depends_on: {postgres: {condition: service_healthy}} + ports: ["8099:8099"] + dc1-dior: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: dior, SERVER_PORT: "8092", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1} + depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc1-hermes: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: hermes, SERVER_PORT: "8093", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1} + depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc1-prada: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: prada, SERVER_PORT: "8094", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1} + depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc1-chanel: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: chanel, SERVER_PORT: "8091", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1} + depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc1-gucci: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: gucci, SERVER_PORT: "8095", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1, CHANEL_URL: "http://dc1-chanel:8091"} + depends_on: {dc1-chanel: {condition: service_healthy}} + ports: ["8095:8095"] + + # ================= DC2 ================= + dc2-louisvuitton: + <<: [*backend, *svc-health] + container_name: rtgs-dc2-louisvuitton + environment: {<<: *backend-env, SERVICE: louisvuitton, SERVER_PORT: "8099", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2, RTGS_CONTROL_ENABLED: "false"} + depends_on: {postgres: {condition: service_healthy}} + ports: ["8199:8099"] + dc2-dior: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: dior, SERVER_PORT: "8092", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2} + depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc2-hermes: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: hermes, SERVER_PORT: "8093", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2} + depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc2-prada: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: prada, SERVER_PORT: "8094", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2} + depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc2-chanel: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: chanel, SERVER_PORT: "8091", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2} + depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc2-gucci: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: gucci, SERVER_PORT: "8095", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2, CHANEL_URL: "http://dc2-chanel:8091"} + depends_on: {dc2-chanel: {condition: service_healthy}} + ports: ["8195:8095"] + + # ================= DC3 ================= + dc3-louisvuitton: + <<: [*backend, *svc-health] + container_name: rtgs-dc3-louisvuitton + environment: {<<: *backend-env, SERVICE: louisvuitton, SERVER_PORT: "8099", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3, RTGS_CONTROL_ENABLED: "false"} + depends_on: {postgres: {condition: service_healthy}} + ports: ["8299:8099"] + dc3-dior: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: dior, SERVER_PORT: "8092", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3} + depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc3-hermes: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: hermes, SERVER_PORT: "8093", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3} + depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc3-prada: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: prada, SERVER_PORT: "8094", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3} + depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc3-chanel: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: chanel, SERVER_PORT: "8091", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3} + depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}} + dc3-gucci: + <<: [*backend, *svc-health] + environment: {<<: *backend-env, SERVICE: gucci, SERVER_PORT: "8095", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3, CHANEL_URL: "http://dc3-chanel:8091"} + depends_on: {dc3-chanel: {condition: service_healthy}} + ports: ["8295:8095"] + + # ================= 프론트(콘솔 + DT) ================= + frontend: + image: rtgs-frontend:latest + build: + context: .. + dockerfile: deploy/Dockerfile.frontend + container_name: rtgs-frontend + restart: unless-stopped + ports: ["8080:80"] + depends_on: + dc1-gucci: {condition: service_healthy} + +volumes: + pgdata: diff --git a/deploy/init/init-3centers.sql b/deploy/init/init-3centers.sql new file mode 100644 index 0000000..2f6e02d --- /dev/null +++ b/deploy/init/init-3centers.sql @@ -0,0 +1,8 @@ +-- 3센터 삼중화용 DB 3개 생성. 스키마·시드는 각 센터 LouisVuitton 기동 시 Flyway가 담당(V1~V5). +-- postgres 컨테이너 최초 기동 시 1회 실행(docker-entrypoint-initdb.d). 사용자 rtgs 는 compose env로 생성됨. +CREATE DATABASE rtgs_dc1; +CREATE DATABASE rtgs_dc2; +CREATE DATABASE rtgs_dc3; +GRANT ALL PRIVILEGES ON DATABASE rtgs_dc1 TO rtgs; +GRANT ALL PRIVILEGES ON DATABASE rtgs_dc2 TO rtgs; +GRANT ALL PRIVILEGES ON DATABASE rtgs_dc3 TO rtgs; diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..3c3ee2f --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,45 @@ +# RTGS 프론트(콘솔 + DT) 정적 서빙 + API 리버스프록시. +# dev의 vite proxy를 대체: 콘솔은 X-RTGS-DC 헤더로, DT는 /dcN/ 경로로 대상 센터를 고른다. +# 컨테이너 DNS(127.0.0.11)로 service name 동적 해석 → proxy_pass 변수 사용. + +# 콘솔용 센터 라우팅(요청 헤더 X-RTGS-DC 기준). 기본 DC1. +map $http_x_rtgs_dc $chanel_up { default "dc1-chanel:8091"; "DC2" "dc2-chanel:8091"; "DC3" "dc3-chanel:8091"; } +map $http_x_rtgs_dc $lv_up { default "dc1-louisvuitton:8099"; "DC2" "dc2-louisvuitton:8099"; "DC3" "dc3-louisvuitton:8099"; } +map $http_x_rtgs_dc $gucci_up { default "dc1-gucci:8095"; "DC2" "dc2-gucci:8095"; "DC3" "dc3-gucci:8095"; } + +server { + listen 80; + server_name _; + resolver 127.0.0.11 valid=10s; # Docker/compose 내장 DNS (service name 동적 해석) + + root /usr/share/nginx/html; + index index.html; + + # --- DT 단말: /dc1|/dc2|/dc3/... → 해당 센터 Gucci (prefix 제거) --- + location ~ ^/dc([123])/(.*)$ { + set $dt_gucci "dc$1-gucci:8095"; + proxy_pass http://$dt_gucci/$2$is_args$args; + proxy_set_header Host $host; + } + + # --- 콘솔 API: Chanel 계열 --- + location ~ ^/(accounts|inquiry|pay|meta|rawmessage|notifications|ledger)(/|$) { + proxy_pass http://$chanel_up; + proxy_set_header Host $host; + } + # --- 콘솔 API: 관리(LouisVuitton) --- + location ~ ^/(admin|console)(/|$) { + proxy_pass http://$lv_up; + proxy_set_header Host $host; + } + # --- 콘솔 API: Gucci(로그인 등) --- + location /gucci/ { + proxy_pass http://$gucci_up; + proxy_set_header Host $host; + } + + # --- 정적 파일 + SPA 폴백(콘솔). dt.html 은 그대로 서빙 --- + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7b46505..51dd30a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -13,6 +13,12 @@ const routed = (basePort: number) => ({ export default defineConfig({ plugins: [react()], + // 배포 빌드: 콘솔(index.html) + 단말(dt.html) 두 엔트리를 한 번에 dist로. + build: { + rollupOptions: { + input: { main: 'index.html', dt: 'dt.html' }, + }, + }, server: { port: 5174, proxy: {