Files
ga-commission-system/DEVELOPER_GUIDE.md
T
2026-05-12 22:16:28 +09:00

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)

  1. File > Openga-commission-system 선택 → Gradle 자동 임포트
  2. Settings > Build > Compiler > Annotation ProcessorsEnable
  3. Settings > PluginsLombok 설치/활성화
  4. 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 자동 변환)
  • SearchParampageNum/pageSize/searchKeyword/startDate/endDate/sortField/sortOrder
  • @RequirePermission(menu, perm) — AOP 권한 체크
  • @DataChangeLog(menu, table) — 변경 이력 자동 기록
  • CommonCodeService — Redis 캐시 기반 코드 변환 (getCodeName, validateCode)
  • ExcelServiceexportLargeExcel (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:1
  • ProductGroupResp.java — 화면 응답 (조인 필드 포함)
  • ProductGroupSaveReq.java — 입력 (@NotBlank, @Size)
  • ProductGroupSearchParam.javaextends 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.tsxcomponentMap 에 키도 추가:

'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)
  • XxxSearchParamextends SearchParam
  • XxxExcelVO — 엑셀 전용 (@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")menumenu 테이블에 등록됐는지 확인
  • 사용자 역할에 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=truemybatis-config.xml 에 적용됐는지 확인
  • 컬럼명과 필드명 매핑이 안 맞으면 <resultMap> 명시

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. 자주 묻는 운영 작업

비밀번호 강제 초기화

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