# 07_INFRA_SPEC — Docker / Nginx / 문서 ## 1. docker-compose.yml 루트에 위치. 5 서비스: `postgres / redis / api / batch / frontend`. ```yaml services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: ga POSTGRES_USER: ga POSTGRES_PASSWORD: ga TZ: Asia/Seoul ports: ["5432:5432"] volumes: [ga-postgres-data:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U ga -d ga"] interval: 10s redis: image: redis:7-alpine ports: ["6379:6379"] volumes: [ga-redis-data:/data] api: build: context: . dockerfile: Dockerfile args: { MODULE: ga-api } depends_on: postgres: { condition: service_healthy } redis: { condition: service_started } environment: SPRING_PROFILES_ACTIVE: docker SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ga SPRING_DATASOURCE_USERNAME: ga SPRING_DATASOURCE_PASSWORD: ga SPRING_DATA_REDIS_HOST: redis JWT_SECRET: ${JWT_SECRET:-please-change-this-jwt-secret-must-be-256-bits} ENCRYPT_KEY: ${ENCRYPT_KEY:-please-change-this-32-byte-key!} ports: ["8080:8080"] batch: build: { context: ., dockerfile: Dockerfile, args: { MODULE: ga-batch } } depends_on: postgres: { condition: service_healthy } redis: { condition: service_started } api: { condition: service_started } environment: # api 와 동일 ... ports: ["8081:8081"] frontend: build: { context: ./ga-frontend, dockerfile: Dockerfile } depends_on: [api] ports: ["3000:80"] volumes: { ga-postgres-data: , ga-redis-data: } ``` --- ## 2. Dockerfile (루트, ga-api/ga-batch 공용) 멀티스테이지: Gradle 빌드 → 슬림 JRE. ```dockerfile # stage 1: builder FROM gradle:8.6-jdk17 AS builder WORKDIR /workspace COPY settings.gradle build.gradle gradle.properties ./ COPY ga-common/build.gradle ga-common/ COPY ga-core/build.gradle ga-core/ COPY ga-api/build.gradle ga-api/ COPY ga-batch/build.gradle ga-batch/ COPY ga-admin/build.gradle ga-admin/ RUN gradle dependencies --no-daemon || true COPY . . ARG MODULE=ga-api RUN gradle :${MODULE}:bootJar --no-daemon -x test RUN cp /workspace/${MODULE}/build/libs/${MODULE}.jar /workspace/app.jar # stage 2: runtime FROM eclipse-temurin:17-jre-alpine WORKDIR /app RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime COPY --from=builder /workspace/app.jar /app/app.jar EXPOSE 8080 8081 ENTRYPOINT ["java","-XX:+UseContainerSupport","-XX:MaxRAMPercentage=75","-jar","/app/app.jar"] ``` --- ## 3. ga-frontend/Dockerfile + nginx.conf ```dockerfile # ga-frontend/Dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci || npm install COPY . . RUN npm run build # → /app/dist FROM nginx:1.27-alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 ``` ```nginx # ga-frontend/nginx.conf server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # SPA fallback location / { try_files $uri $uri/ /index.html; } # API 프록시 location /api/ { proxy_pass http://api:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 100m; proxy_read_timeout 300s; # 엑셀 대용량 다운로드 대비 } # 정적 자원 캐시 location ~* \.(?:css|js|woff2?|ttf|svg|png|jpg|jpeg|gif|ico)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; } } ``` --- ## 4. README.md (루트) 작성 항목 (현재 README.md 가 이미 갖춘 형태): - 기술 스택 요약 표 - 모듈 구조 다이어그램 - **시나리오 A — 운영 DB(`192.168.0.60:55432/trading_ai/ga`) 연결** - Gradle wrapper 생성 → `bootJar` → `bootRun --args='--spring.profiles.active=trading_ai'` - 환경변수 `DB_PASSWORD`, `REDIS_HOST` 오버라이드 - **시나리오 B — 로컬 PostgreSQL** (개발용) - `docker compose up -d postgres redis` → `bootRun` - **시나리오 C — Docker Compose 전체** (운영 흐름) - `docker compose up -d --build` - DB 마이그레이션 직접 실행 (Python `migrate.py`) - 데이터 확인 표 (보험사 5 / 상품 20 / 설계사 50 / 계약 200 / ...) - 트러블슈팅 FAQ - `gradle wrapper --gradle-version 8.6` - `Failed to determine a suitable driver class` → 프로파일 확인 - `NoSuchKey: ga` → `currentSchema=ga` - 401 토큰 만료 → 재로그인 - AG Grid 빈 화면 → CSS import 확인 - 주요 기능 (백엔드/도메인/배치 8 Step 요약) - 보안 (JWT/AES/RBAC/감사로그) - 진행 상황 체크리스트 - 향후 확장 - 외부 시스템 연결 정보 --- ## 5. DEVELOPER_GUIDE.md (신입 개발자 온보딩) ### 1. 환경 설정 - JDK 17 (`java -version` 확인) - Node 20 + npm - PostgreSQL 14+ (운영 또는 Docker) - Redis 7+ (선택) - IntelliJ IDEA Community + Lombok 플러그인 (자동 설치 권장) ### 2. 프로젝트 구조 한 장 다이어그램 ``` ga-common ─┬─> ga-core ─┬─> ga-api (REST API, 8080) │ ├─> ga-batch (Spring Batch, 8081) │ └─> ga-admin (예약 모듈) └─> 다른 모듈 모두 의존 (web/security/aop/cache/mybatis/excel...) ga-frontend (독립 빌드, dist → nginx) ``` ### 3. 새 화면 만들기 Step-by-Step (예: "공지사항") **Step 1 — DB**: `notice` 테이블 + 메뉴/권한 등록 (V18 마이그레이션) **Step 2 — VO** (`ga-core/vo/notice/`): - `NoticeVO.java` (테이블 1:1) - `NoticeResp.java` (조인 결과) - `NoticeSaveReq.java` (`@Valid` 검증) - `NoticeSearchParam.java extends SearchParam` **Step 3 — Mapper**: - `mapper/notice/NoticeMapper.java` (`@Mapper`) - `resources/mapper/notice/NoticeMapper.xml` (기존 AgentMapper.xml 복사 후 수정) **Step 4 — Service** (`ga-api/service/notice/`): - `NoticeService.java` (단순 CRUD 패턴 + `@Transactional`) **Step 5 — Controller**: - `NoticeController.java` — `@RequirePermission(menu="NOTICE", perm="...")` - CUD 메서드에 `@DataChangeLog` **Step 6 — 프론트** (`ga-frontend/src/`): - `api/notice.ts` — 함수 5개 (list/detail/create/update/delete) - `pages/notice/NoticeList.tsx` — `06_FRONTEND_SPEC.md §7` 패턴 - `pages/notice/NoticeForm.tsx` — Ant Form **Step 7 — 메뉴 등록**: - `system/menus` 화면에서 메뉴/권한 등록 → 사용자 역할에 권한 부여 ### 4. 자주 쓰는 패턴 - **공통코드 드롭다운**: `` - **엑셀 다운로드**: `` - **상태 뱃지**: `` - **권한 버튼**: `...` - **AG Grid 서버사이드**: ` api.list(p)} />` ### 5. 코드 컨벤션 - VO 네이밍: `XxxVO / XxxSearchParam / XxxSaveReq / XxxResp / XxxExcelVO` - Mapper XML: `XxxMapper.xml` (resources/mapper/{도메인}/) - API URL: `/api/{리소스}s` (복수형, kebab-case) - 금액: 항상 `BigDecimal` + `MoneyUtil` 헬퍼 - 날짜: `LocalDate / LocalDateTime` (java.util.Date 금지) - 예외: `BizException(ErrorCode.XXX)` (RuntimeException 직접 throw 금지) - 로그: `lombok @Slf4j` + `log.info("msg, key={}", value)` (한글 메시지 OK) ### 6. 트러블슈팅 FAQ - **권한 에러**: `@RequirePermission` 의 menuCode 가 `menu_permission` 에 등록되어 있는지 + 사용자 역할에 권한 부여됐는지 - **공통코드 캐시 안 맞음**: `CommonCodeService.refreshCache(groupCode)` 또는 Redis FLUSH - **MyBatis 매핑 에러**: ResultMap 의 column / property 명 + `mapUnderscoreToCamelCase` 확인 - **암호화 컬럼 평문 노출**: `EncryptTypeHandler` 가 컬럼별 명시(typeHandler 속성)되어 있는지 - **JSONB null**: `JsonTypeHandler` 가 `@MappedTypes(JsonNode.class)` 로 등록됐는지 - **PageHelper 안 먹음**: `param.startPage()` 호출 후 SELECT 1회만 + 같은 트랜잭션 - **AG Grid 빈 화면**: `@ag-grid-community/styles/ag-theme-quartz.css` import 확인 --- ## 6. 통합 검증 체크리스트 ```bash # 1) 빌드 ./gradlew clean build # 모든 모듈 컴파일 + 단위 테스트 통과 cd ga-frontend && npm install && npm run build # → dist/ # 2) Docker 전체 기동 docker compose up -d --build # 3) Smoke curl http://localhost:8080/api/health # {"status":"UP"} curl http://localhost:3000/ # nginx 200 # 브라우저: admin / admin1234! 로그인 → 메뉴 트리 보임 → 설계사 50 행 표시 # 4) 정산 배치 curl -X POST -H "Authorization: Bearer $TOKEN" \ "http://localhost:8081/api/batch/settlement/run?settleMonth=202605" # 5) 태그 git tag v1.0.0-alpha ``` 기대 결과: - Flyway V1~V17 자동 적용 - ga-api 8080 / ga-batch 8081 / frontend 3000 모두 OK - 단위테스트 76+ 건 통과 - 운영 DB 시나리오 A 도 동일 동작 (`--spring.profiles.active=trading_ai`)