# 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. 자주 쓰는 패턴
- **공통코드 드롭다운**: `<CodeSelect groupCode="..." />`
- **엑셀 다운로드**: `<ExcelExportButton url={...} fileName={...} showProgress />`
- **상태 뱃지**: `<CodeBadge groupCode="..." value={status} />`
- **권한 버튼**: `<PermissionButton menuCode permCode>...</PermissionButton>`
- **AG Grid 서버사이드**: `<DataGrid dataSource={p => 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`)
