11 KiB
11 KiB
GA 수수료 정산 솔루션 — 신입 개발자 가이드
처음 합류한 개발자가 30분 안에 로컬 환경을 띄우고, 새 화면 1개를 추가할 수 있도록 안내합니다.
1. 환경 설정
필수 설치
| 도구 | 버전 | 비고 |
|---|---|---|
| JDK | 17+ | java -version |
| Node.js | 18+ | node -v |
| PostgreSQL | 14+ | 로컬 또는 운영 DB 접속 |
| Redis | 7+ | 공통코드 캐시 (선택) |
| IntelliJ IDEA | Community 이상 | Lombok 플러그인 활성화 필수 |
IDE 설정 (IntelliJ)
File > Open→ga-commission-system선택 → Gradle 자동 임포트Settings > Build > Compiler > Annotation Processors→ EnableSettings > Plugins→ Lombok 설치/활성화- JDK 17 지정 (
Project Structure > SDK)
빠른 기동
# 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<T>— 모든 API 표준 응답PageResponse<T>— 페이징 응답 (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)
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:1ProductGroupResp.java— 화면 응답 (조인 필드 포함)ProductGroupSaveReq.java— 입력 (@NotBlank,@Size)ProductGroupSearchParam.java—extends SearchParam
Step 3 — Mapper Interface + XML
@Mapper
public interface ProductGroupMapper {
List<ProductGroupResp> 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:
<select id="selectList" resultType="com.ga.core.vo.product.ProductGroupResp">
SELECT pg.* FROM product_group pg
<where>
<if test="searchKeyword != null and searchKeyword != ''">
AND pg.group_name LIKE CONCAT('%', #{searchKeyword}, '%')
</if>
</where>
ORDER BY pg.sort_order
</select>
Step 4 — Service
@Service @RequiredArgsConstructor
public class ProductGroupService {
private final ProductGroupMapper mapper;
public PageResponse<ProductGroupResp> list(ProductGroupSearchParam p) {
p.startPage();
return PageResponse.of(mapper.selectList(p));
}
@Transactional
public Long create(ProductGroupSaveReq req) { /* ... */ }
}
Step 5 — Controller (ga-api)
@RestController @RequestMapping("/api/product-groups")
@Tag(name = "상품그룹") @RequiredArgsConstructor
public class ProductGroupController {
private final ProductGroupService service;
@GetMapping
@RequirePermission(menu = "PRODUCT_GROUP", perm = "READ")
public ApiResponse<PageResponse<ProductGroupResp>> 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<Long> create(@Valid @RequestBody ProductGroupSaveReq req) {
return ApiResponse.ok(service.create(req));
}
}
Step 6 — React 페이지 (ga-frontend)
src/api/productGroup.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:
export default function ProductGroupList() {
const [param, setParam] = useState({ pageNum: 1, pageSize: 20 });
const { data } = useQuery({ queryKey: ['pg', param], queryFn: () => productGroupApi.list(param) });
return (
<>
<SearchForm conditions={[{ type: 'text', name: 'searchKeyword', label: '그룹명' }]}
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })} />
<PermissionButton menu="PRODUCT_GROUP" perm="CREATE" type="primary">신규</PermissionButton>
<DataTable data={data} page={param} onPageChange={(p) => setParam({ ...param, ...p })}
columns={[{ title: '그룹명', dataIndex: 'groupName' }]} />
</>
);
}
라우터 등록 — App.tsx:
<Route path="system/product-groups" element={<ProductGroupList />} />
router/dynamicRoutes.tsx 의 componentMap 에 키도 추가:
'system/ProductGroupList': lazy(() => import('@/pages/system/ProductGroupList')),
끝! 메뉴 권한이 있는 사용자에게만 좌측 메뉴가 보이고, READ/CREATE 버튼이 권한별로 렌더됩니다.
4. 자주 쓰는 패턴
공통코드 드롭다운
<CodeSelect groupCode="AGENT_STATUS" value={status} onChange={setStatus} includeAll />
상태 뱃지
<CodeBadge groupCode="CONTRACT_STATUS" code={row.status} />
권한 버튼
<PermissionButton menu="ORG_AGENT" perm="UPDATE" onClick={save}>저장</PermissionButton>
권한 없으면 자동으로 disabled 처리. AOP @RequirePermission 와 동일한 메뉴×권한 매트릭스 사용.
엑셀 다운로드 버튼
<ExcelExportButton url="/api/agents/excel" filename="설계사목록" params={searchParam} />
엑셀 업로드 (템플릿 매핑 기반)
<ExcelImportButton templateCode="SAMSUNG_RECRUIT" onComplete={refetch} />
AG Grid 서버사이드 페이징
<DataGrid
rowModelType="serverSide"
serverSideStoreType="partial"
cacheBlockSize={100}
fetchUrl="/api/ledger/recruit"
columnDefs={[/*...*/]}
/>
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 SearchParamXxxExcelVO— 엑셀 전용 (@ExcelColumn)
Mapper XML
SELECT *금지 — 컬럼 명시- 목록은
XxxResp, INSERT/UPDATE 는XxxVO매핑 - 동적 WHERE 는
<where>+<if> - 1:N 은
<resultMap><collection> - 자기참조 트리는
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에 적용됐는지 확인- 컬럼명과 필드명 매핑이 안 맞으면
<resultMap>명시
Q. EncryptTypeHandler 가 적용 안 됨
- VO 매퍼에
typeHandler="com.ga.common.mybatis.EncryptTypeHandler"명시 - 또는 컬럼명 단위로
#{accountNo,typeHandler=...}명시
Q. 엑셀 70만건 업로드 중 OOM
xlsx-streamer(SAX) 가 사용됐는지 확인 (StreamingReader.builder())batchSize1000~5000 사이에서 조정- LOOKUP 변환은
LookupCache.preload()로 시작 전 일괄 적재
Q. AG Grid 합계행이 안 보임
pinnedBottomRowData에 합계 행 객체 전달- 또는
enableRangeSelection+aggFunc="sum"컬럼 정의
7. 자주 묻는 운영 작업
비밀번호 강제 초기화
UPDATE users SET password = '$2b$10$...', fail_count = 0 WHERE login_id = 'kyu';
메뉴 권한 재계산 (역할별)
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');
정산 배치 수동 트리거
curl -X POST http://localhost:8080/api/batch/settlement/run \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"settleMonth":"202405"}'
캐시 비우기
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