# 05_API_SPEC — ga-api (REST API) > 모든 Controller / Service 가 동일한 패턴으로 작성된다. > 신입이 복붙 후 테이블명/필드명만 바꾸면 새 화면이 완성되도록. ## 패키지 구조 ``` ga-api/src/main/java/com/ga/api/ ├── GaApiApplication.java # @SpringBootApplication(scanBasePackages = "com.ga") ├── auth/ │ ├── AuthController.java # /api/auth/* │ └── AuthService.java ├── controller/ │ ├── batch/ BatchTriggerController.java │ ├── ledger/ LedgerController.java # 모집/유지/예외 통합 │ ├── org/ OrganizationController.java, AgentController.java │ ├── product/ CompanyController.java, ProductController.java, ContractController.java │ ├── receive/ ReceiveController.java │ ├── rule/ RuleController.java │ ├── settle/ SettleController.java, PaymentController.java │ └── system/ UserController.java, RoleController.java └── service/ └── (controller 미러 구조 — XxxService.java) ``` build.gradle: ```groovy apply plugin: 'org.springframework.boot' bootJar { archiveFileName = 'ga-api.jar'; mainClass = 'com.ga.api.GaApiApplication' } jar.enabled = false dependencies { implementation project(':ga-core') } ``` `application.yml` 은 `01_DB_SCHEMA.md` 의 yml + `server.port: 8080`. --- ## 1. 표준 Controller 패턴 ```java @RestController @RequestMapping("/api/agents") @Tag(name = "설계사 관리") @RequiredArgsConstructor public class AgentController { private final AgentService agentService; @GetMapping @RequirePermission(menu = "ORG_AGENT", perm = "READ") public ApiResponse> list(AgentSearchParam param) { return ApiResponse.ok(agentService.list(param)); } @GetMapping("/{id}") @RequirePermission(menu = "ORG_AGENT", perm = "READ") public ApiResponse detail(@PathVariable Long id) { return ApiResponse.ok(agentService.detail(id)); } @PostMapping @RequirePermission(menu = "ORG_AGENT", perm = "CREATE") @DataChangeLog(menu = "ORG_AGENT", table = "agent") public ApiResponse create(@Valid @RequestBody AgentSaveReq req) { return ApiResponse.ok(agentService.create(req)); } @PutMapping("/{id}") @RequirePermission(menu = "ORG_AGENT", perm = "UPDATE") @DataChangeLog(menu = "ORG_AGENT", table = "agent") public ApiResponse update(@PathVariable Long id, @Valid @RequestBody AgentSaveReq req) { agentService.update(id, req); return ApiResponse.ok(); } @PatchMapping("/{id}/status") @RequirePermission(menu = "ORG_AGENT", perm = "UPDATE") @DataChangeLog(menu = "ORG_AGENT", table = "agent") public ApiResponse updateStatus(@PathVariable Long id, @RequestParam String status) { agentService.updateStatus(id, status); return ApiResponse.ok(); } @GetMapping("/export") @RequirePermission(menu = "ORG_AGENT", perm = "EXPORT") public void export(AgentSearchParam p, HttpServletResponse res) { agentService.exportExcel(p, res); } } ``` 규칙: 1. **클래스 어노테이션**: `@RestController @RequestMapping @Tag @RequiredArgsConstructor` 2. **메서드 어노테이션**: `@RequirePermission` + (CUD 일 때) `@DataChangeLog` 3. **응답**: `ApiResponse` / `ApiResponse>` / `ApiResponse` 4. **요청 바디**: `@Valid @RequestBody XxxSaveReq` 5. **검색**: `XxxSearchParam` (쿼리 파라미터로 자동 바인딩) 6. **PathVariable**: id 는 `Long` 7. **엑셀 export 는 `void` + `HttpServletResponse` 직접 사용** --- ## 2. 표준 Service 패턴 ```java @Service @RequiredArgsConstructor public class AgentService { private final AgentMapper mapper; private final CommonCodeService codeService; private final AgentMapstruct mapstruct; private final ExcelService excelService; public PageResponse list(AgentSearchParam param) { param.startPage(); return PageResponse.of(mapper.selectList(param)); } public AgentResp detail(Long id) { AgentResp r = mapper.selectDetailById(id); if (r == null) throw new BizException(ErrorCode.NOT_FOUND); return r; } @Transactional public Long create(AgentSaveReq req) { if (req.getLicenseNo() != null && mapper.existsByLicenseNo(req.getLicenseNo()) > 0) throw new BizException(ErrorCode.DUPLICATE_DATA); AgentVO vo = mapstruct.toVO(req); vo.setStatus("ACTIVE"); mapper.insert(vo); return vo.getAgentId(); } @Transactional public void update(Long id, AgentSaveReq req) { AgentVO vo = mapper.selectById(id); if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); mapstruct.update(vo, req); mapper.update(vo); } @Transactional public void updateStatus(Long id, String status) { codeService.validateCode("AGENT_STATUS", status); mapper.updateStatus(id, status); } public void exportExcel(AgentSearchParam p, HttpServletResponse res) { excelService.exportLargeExcel("설계사목록", AgentExcelVO.class, () -> mapper.selectCursorForExcel(p), res); } } ``` 규칙: 1. **Service 는 Mapper + 다른 Service 만 의존** (Controller 직접 호출 X) 2. **CUD 메서드는 `@Transactional`** 3. **공통코드 검증은 `codeService.validateCode(group, code)`** 4. **중복 검증 → `DUPLICATE_DATA`, 미존재 → `NOT_FOUND`** 5. **엑셀은 `excelService.exportLargeExcel(fileName, ExcelVO.class, cursorSupplier, response)`** --- ## 3. API 엔드포인트 목록 (현재 구현된 것) ### 인증 (`AuthController` — `/api/auth`) - `POST /login` — `{loginId, password}` → `{accessToken, refreshToken, user}` + `login_log` 적재 - `POST /refresh` — refresh 토큰으로 access 갱신 - `POST /logout` — 클라이언트 토큰 폐기 (서버는 로그만) - `GET /me` — 현재 사용자 + 역할 + 메뉴 트리 ### 조직 / 설계사 - `OrganizationController /api/orgs` - `GET /tree` — 트리 (CTE 재귀) - `GET /{id}` / `POST` / `PUT /{id}` / `DELETE /{id}` - `AgentController /api/agents` - `GET` (검색+페이징) / `GET /{id}` / `POST` / `PUT /{id}` - `PATCH /{id}/status` / `PATCH /{id}/transfer` (조직/직급 변경) - `GET /{id}/history` (조직/직급 이력) - `GET /export` (엑셀 다운로드) ### 보험상품 / 계약 - `CompanyController /api/companies` — 보험사 5 건 CRUD - `ProductController /api/products` — 보험사 × 상품 - `ContractController /api/contracts` — 검색/CRUD/엑셀, `policy_no` 중복검증 ### 수수료 규정 - `RuleController /api/rules` - `GET /commission-rates`, `POST`, `PUT /{id}`, `DELETE /{id}` - `GET /payout-rules`, ... 동일 패턴 - `GET /override-rules`, `GET /chargeback-rules`, `GET /exception-codes` ### 데이터 수신 / 매핑 - `ReceiveController /api/receive` - `POST /upload?companyCode=&dataType=` — multipart 업로드 → `raw_commission_data` 적재 - `POST /process?companyCode=&settleMonth=` — `MappingEngine` 으로 변환 호출 - `GET /status?companyCode=&settleMonth=` — 진행 현황 - `GET /errors?companyCode=&resolveStatus=` — 에러 목록 - `PATCH /errors/{errorId}/resolve` — 에러 해결 처리 - 매핑 관리: `GET /mapping/{companyCode}/fields`, `POST/PUT/DELETE` ### 원장 (`LedgerController /api/ledger` — 통합) - `GET /recruit` (검색+페이징) / `GET /recruit/export` (엑셀) - `GET /maintain`, `GET /exception` — 동일 패턴 - `POST /exception` — 예외금액 등록 (수동) - `PATCH /exception/{id}/approve` — 승인 (`@RequirePermission perm=APPROVE`) - `PATCH /exception/{id}/reject` — 반려 ### 정산 - `SettleController /api/settle` - `GET` (월별 검색) / `GET /{id}` / `GET /summary?settleMonth=` - `PATCH /{id}/confirm` (`@RequirePermission APPROVE`) — `@Transactional`, `version` 체크 → `RULE_VERSION_CONFLICT` 가능 - `PATCH /{id}/hold` `{reason}` - `PaymentController /api/payments` - `GET` 검색 / `POST` 이체 생성 (`settle_master.status='CONFIRMED'` 인 것만) - `POST /{id}/transfer-file` 이체파일 생성 - `PATCH /{id}/success`, `PATCH /{id}/fail` ### 배치 트리거 (`BatchTriggerController /api/batch`) - `POST /settlement/run?settleMonth=&companyCode=` — `batch_job_log` 적재 + ga-batch 트리거 - `GET /settlement/status?jobLogId=` — 진행 상태 - `POST /settlement/restart?jobLogId=&fromStep=` — 재실행 (선택 구현) ### 시스템관리 - `UserController /api/system/users` - `GET` / `POST` / `PUT /{id}` / `DELETE /{id}` - `PATCH /{id}/reset-password` — 임시 비밀번호 발급 (이메일/알림) - `PATCH /{id}/unlock` — 잠금 해제 - `PUT /{id}/roles` — 역할 매핑 - `RoleController /api/system/roles` - 역할 CRUD + `GET /{id}/permissions` (메뉴 × 권한 매트릭스) - `PUT /{id}/permissions` (전체 매트릭스 갱신) ### 공통 (ga-common 에 위치하지만 ga-api 가 노출) - `MenuController /api/menus` — `GET /my`, `GET /tree`, CRUD - `CommonCodeController /api/common/codes` — 그룹/상세 CRUD - `FileController /api/files` — 업로드/다운로드/목록 - `NotificationController /api/notifications` — 목록/읽음 - `SystemConfigController /api/system/config` — 시스템 설정 - `SystemLogController /api/system/logs` — 로그인/API/변경 이력 --- ## 4. 인증 / 권한 흐름 1. **로그인**: `POST /api/auth/login` → JWT 발급 2. **헤더**: 모든 요청에 `Authorization: Bearer ` 3. **`JwtAuthFilter`**: SecurityContext 에 `LoginUser` 주입 4. **`@RequirePermission`**: `PermissionAspect` 가 `role_menu_permission` 조회 → 없으면 `FORBIDDEN` 5. **`@DataChangeLog`**: 변경 전/후 데이터 비교 후 `data_change_log` JSONB 비동기 적재 6. **`ApiLogAspect`**: 모든 요청 `api_access_log` 비동기 적재 (민감정보 마스킹) --- ## 5. 응답 표준 ### 성공 ```json { "success": true, "code": "0000", "message": "OK", "data": { ... }, "timestamp": "2026-05-10T12:34:56" } ``` ### 페이징 ```json { "success": true, "code": "0000", "data": { "list": [...], "pageNum": 1, "pageSize": 20, "total": 123, "pages": 7 } } ``` ### 실패 ```json { "success": false, "code": "B009", "message": "중복된 데이터입니다", "data": null, "timestamp": "..." } ``` --- ## 6. 테스트 (선택) - MockMvc 기반 Controller 테스트 - `@WebMvcTest(AgentController.class)` + `@MockBean AgentService` - 인증/권한 검증 (403 응답) - 요청 바디 검증 (`@Valid` 실패 시 400) - WireMock 으로 ga-batch 호출 stub