From 46d47812f8328d40d44a474ab1039b8475f3e5f3 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Tue, 12 May 2026 22:16:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEVELOPER_GUIDE.md | 336 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 DEVELOPER_GUIDE.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..6e92e33 --- /dev/null +++ b/DEVELOPER_GUIDE.md @@ -0,0 +1,336 @@ +# GA 수수료 정산 솔루션 — 신입 개발자 가이드 + +> 처음 합류한 개발자가 30분 안에 로컬 환경을 띄우고, 새 화면 1개를 추가할 수 있도록 안내합니다. + +--- + +## 1. 환경 설정 + +### 필수 설치 +| 도구 | 버전 | 비고 | +|------|------|------| +| **JDK** | 17+ | `java -version` | +| **Node.js** | 18+ | `node -v` | +| **PostgreSQL** | 14+ | 로컬 또는 운영 DB 접속 | +| **Redis** | 7+ | 공통코드 캐시 (선택) | +| **IntelliJ IDEA** | Community 이상 | Lombok 플러그인 활성화 필수 | + +### IDE 설정 (IntelliJ) +1. `File > Open` → `ga-commission-system` 선택 → Gradle 자동 임포트 +2. `Settings > Build > Compiler > Annotation Processors` → **Enable** +3. `Settings > Plugins` → **Lombok** 설치/활성화 +4. JDK 17 지정 (`Project Structure > SDK`) + +### 빠른 기동 +```bash +# 1) 인프라 (PostgreSQL + Redis) +docker compose up -d postgres redis + +# 2) 백엔드 (ga-api 8080, ga-batch 8081 별도 콘솔) +./gradlew :ga-api:bootRun +./gradlew :ga-batch:bootRun + +# 3) 프론트엔드 (Vite dev server, 3000) +cd ga-frontend && npm install && npm run dev +``` + +로그인: `admin / admin1234!` + +--- + +## 2. 프로젝트 구조 + +``` +ga-commission-system/ +├── ga-common/ 공통 프레임워크 — 인증/AOP/엑셀/공통코드/메뉴/유틸 +├── ga-core/ 도메인 코어 — VO + Mapper Interface + Mapper XML + MapStruct +├── ga-api/ REST 컨트롤러 + 서비스 (포트 8080) +├── ga-batch/ Spring Batch 정산 잡 (포트 8081) +└── ga-frontend/ React + Vite + Ant Design + AG Grid (포트 3000) +``` + +### ga-common 핵심 클래스 +- **`ApiResponse`** — 모든 API 표준 응답 +- **`PageResponse`** — 페이징 응답 (PageHelper 자동 변환) +- **`SearchParam`** — `pageNum/pageSize/searchKeyword/startDate/endDate/sortField/sortOrder` +- **`@RequirePermission(menu, perm)`** — AOP 권한 체크 +- **`@DataChangeLog(menu, table)`** — 변경 이력 자동 기록 +- **`CommonCodeService`** — Redis 캐시 기반 코드 변환 (`getCodeName`, `validateCode`) +- **`ExcelService`** — `exportLargeExcel` (100만건), `importLargeExcel` (70만건) + +--- + +## 3. 새 화면 만들기 — Step-by-Step (5분 컷) + +예시: **상품 그룹 관리** 화면 추가. + +### Step 1 — 메뉴 등록 (DB) +```sql +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, + component_path, menu_level, sort_order, is_visible, is_active) +VALUES ((SELECT menu_id FROM menu WHERE menu_code='SYSTEM'), + 'PRODUCT_GROUP', '상품그룹', 'PAGE', '/system/product-groups', + 'system/ProductGroupList', 2, 99, 'Y', 'Y'); + +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT menu_id, 'READ', '조회', 1 FROM menu WHERE menu_code = 'PRODUCT_GROUP' +UNION ALL SELECT menu_id, 'CREATE', '등록', 2 FROM menu WHERE menu_code = 'PRODUCT_GROUP' +UNION ALL SELECT menu_id, 'UPDATE', '수정', 3 FROM menu WHERE menu_code = 'PRODUCT_GROUP' +UNION ALL SELECT menu_id, 'DELETE', '삭제', 4 FROM menu WHERE menu_code = 'PRODUCT_GROUP'; + +-- 관리자 역할에 권한 부여 +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, m.menu_id, p.perm_code, 'Y' + FROM role r CROSS JOIN menu m JOIN menu_permission p ON p.menu_id = m.menu_id + WHERE r.role_code = 'SUPER_ADMIN' AND m.menu_code = 'PRODUCT_GROUP'; +``` + +### Step 2 — VO 작성 (`ga-core`) +- `ProductGroupVO.java` — 테이블 1:1 +- `ProductGroupResp.java` — 화면 응답 (조인 필드 포함) +- `ProductGroupSaveReq.java` — 입력 (`@NotBlank`, `@Size`) +- `ProductGroupSearchParam.java` — `extends SearchParam` + +### Step 3 — Mapper Interface + XML +```java +@Mapper +public interface ProductGroupMapper { + List selectList(ProductGroupSearchParam p); + ProductGroupVO selectById(@Param("id") Long id); + int insert(ProductGroupVO vo); + int update(ProductGroupVO vo); + int deleteById(@Param("id") Long id); +} +``` + +`ga-core/src/main/resources/mapper/product/ProductGroupMapper.xml`: +```xml + +``` + +### Step 4 — Service +```java +@Service @RequiredArgsConstructor +public class ProductGroupService { + private final ProductGroupMapper mapper; + public PageResponse list(ProductGroupSearchParam p) { + p.startPage(); + return PageResponse.of(mapper.selectList(p)); + } + @Transactional + public Long create(ProductGroupSaveReq req) { /* ... */ } +} +``` + +### Step 5 — Controller (`ga-api`) +```java +@RestController @RequestMapping("/api/product-groups") +@Tag(name = "상품그룹") @RequiredArgsConstructor +public class ProductGroupController { + private final ProductGroupService service; + + @GetMapping + @RequirePermission(menu = "PRODUCT_GROUP", perm = "READ") + public ApiResponse> list(ProductGroupSearchParam p) { + return ApiResponse.ok(service.list(p)); + } + + @PostMapping + @RequirePermission(menu = "PRODUCT_GROUP", perm = "CREATE") + @DataChangeLog(menu = "PRODUCT_GROUP", table = "product_group") + public ApiResponse create(@Valid @RequestBody ProductGroupSaveReq req) { + return ApiResponse.ok(service.create(req)); + } +} +``` + +### Step 6 — React 페이지 (`ga-frontend`) +`src/api/productGroup.ts`: +```ts +export const productGroupApi = { + list: (p: any) => request.get('/api/product-groups', { params: p }), + create: (data: any) => request.post('/api/product-groups', data), +}; +``` + +`src/pages/system/ProductGroupList.tsx`: +```tsx +export default function ProductGroupList() { + const [param, setParam] = useState({ pageNum: 1, pageSize: 20 }); + const { data } = useQuery({ queryKey: ['pg', param], queryFn: () => productGroupApi.list(param) }); + return ( + <> + setParam({ ...param, ...v, pageNum: 1 })} /> + 신규 + setParam({ ...param, ...p })} + columns={[{ title: '그룹명', dataIndex: 'groupName' }]} /> + + ); +} +``` + +라우터 등록 — `App.tsx`: +```tsx +} /> +``` + +`router/dynamicRoutes.tsx` 의 `componentMap` 에 키도 추가: +```ts +'system/ProductGroupList': lazy(() => import('@/pages/system/ProductGroupList')), +``` + +끝! 메뉴 권한이 있는 사용자에게만 좌측 메뉴가 보이고, `READ/CREATE` 버튼이 권한별로 렌더됩니다. + +--- + +## 4. 자주 쓰는 패턴 + +### 공통코드 드롭다운 +```tsx + +``` + +### 상태 뱃지 +```tsx + +``` + +### 권한 버튼 +```tsx +저장 +``` +권한 없으면 자동으로 disabled 처리. AOP `@RequirePermission` 와 동일한 메뉴×권한 매트릭스 사용. + +### 엑셀 다운로드 버튼 +```tsx + +``` + +### 엑셀 업로드 (템플릿 매핑 기반) +```tsx + +``` + +### AG Grid 서버사이드 페이징 +```tsx + +``` + +--- + +## 5. 코드 컨벤션 + +### 네이밍 +| 종류 | 패턴 | 예시 | +|------|------|------| +| 테이블 | snake_case | `agent_org_history` | +| Java 클래스 | PascalCase | `AgentOrgHistoryVO` | +| 필드 | camelCase | `agentName` | +| URL | kebab-case 복수형 | `/api/exception-codes` | +| 메뉴 코드 | UPPER_SNAKE | `RULE_COMMISSION` | +| 공통코드 그룹 | UPPER_SNAKE | `AGENT_STATUS` | + +### VO 5종 패턴 (한 도메인 = 3~5 클래스) +- `XxxVO` — 테이블 1:1 (INSERT/UPDATE) +- `XxxResp` — 응답 (조인 결과 + `*Name` 필드) +- `XxxSaveReq` — 입력 (`@Valid`) +- `XxxSearchParam` — `extends SearchParam` +- `XxxExcelVO` — 엑셀 전용 (`@ExcelColumn`) + +### Mapper XML +- `SELECT *` 금지 — 컬럼 명시 +- 목록은 `XxxResp`, INSERT/UPDATE 는 `XxxVO` 매핑 +- 동적 WHERE 는 `` + `` +- 1:N 은 `` +- 자기참조 트리는 `WITH RECURSIVE` + +### 트랜잭션 +- 읽기 전용 메서드는 어노테이션 없음 (서비스 클래스 기본은 비트랜잭션) +- 쓰기 메서드는 `@Transactional` +- 외부 API 호출과 DB 쓰기를 같이 묶지 말 것 (외부 실패 시 DB 롤백 영향) + +--- + +## 6. 트러블슈팅 FAQ + +### Q. 권한 에러 "FORBIDDEN" +- `@RequirePermission(menu="X", perm="Y")` 의 `menu` 가 `menu` 테이블에 등록됐는지 확인 +- 사용자 역할에 `role_menu_permission` 행이 있는지: `is_granted='Y'` +- 캐시 무효화: `POST /api/system/permission/refresh` 또는 재로그인 + +### Q. 공통코드 변경 후 화면에 반영 안 됨 +- Redis 캐시 TTL — `CommonCodeService.refreshCache()` 호출 +- 또는 관리자 화면 → "캐시 새로고침" + +### Q. MyBatis 매핑 에러 `Could not find result map ...` +- `resultType` 클래스 풀패키지명 확인 +- `mapUnderscoreToCamelCase=true` 가 `mybatis-config.xml` 에 적용됐는지 확인 +- 컬럼명과 필드명 매핑이 안 맞으면 `` 명시 + +### Q. `EncryptTypeHandler` 가 적용 안 됨 +- VO 매퍼에 `typeHandler="com.ga.common.mybatis.EncryptTypeHandler"` 명시 +- 또는 컬럼명 단위로 `#{accountNo,typeHandler=...}` 명시 + +### Q. 엑셀 70만건 업로드 중 OOM +- `xlsx-streamer` (SAX) 가 사용됐는지 확인 (`StreamingReader.builder()`) +- `batchSize` 1000~5000 사이에서 조정 +- LOOKUP 변환은 `LookupCache.preload()` 로 시작 전 일괄 적재 + +### Q. AG Grid 합계행이 안 보임 +- `pinnedBottomRowData` 에 합계 행 객체 전달 +- 또는 `enableRangeSelection` + `aggFunc="sum"` 컬럼 정의 + +--- + +## 7. 자주 묻는 운영 작업 + +### 비밀번호 강제 초기화 +```sql +UPDATE users SET password = '$2b$10$...', fail_count = 0 WHERE login_id = 'kyu'; +``` + +### 메뉴 권한 재계산 (역할별) +```sql +DELETE FROM role_menu_permission WHERE role_id = 2; +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT 2, m.menu_id, p.perm_code, 'Y' + FROM menu m JOIN menu_permission p ON p.menu_id = m.menu_id + WHERE m.menu_code IN ('ORG_AGENT', 'CONTRACT', 'LEDGER_RECRUIT'); +``` + +### 정산 배치 수동 트리거 +```bash +curl -X POST http://localhost:8080/api/batch/settlement/run \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"settleMonth":"202405"}' +``` + +### 캐시 비우기 +```bash +docker exec -it ga-redis redis-cli FLUSHALL +``` + +--- + +## 8. 참고 링크 + +- 운영 DB — `192.168.0.60:55432/trading_ai/ga` +- API 문서 — `http://localhost:8080/swagger-ui.html` +- Git — `https://gl.kyleyang.co.kr/kyu/ga-commission-system` +- 기획 원본 — `GA_솔루션_Agent_Teams_최종통합본.md`