feat: 데이터 도메인 사전 + 데이터 사전 시스템 (V20)

3 agents 병렬 작업 (dba / api / frontend) 결과물 + SQL 버그 2건 fix.

DB (V20__데이터사전.sql):
- data_domain (domain_code PK, category, data_type, length/precision/scale,
  format_pattern, validation_regex, masking_type, encrypted, ...)
- data_dictionary (dict_id PK, table_name, column_name, domain_code FK,
  column_label, is_pk/is_fk/fk_reference, business_rule, sample_value, ...)
- VIEW v_data_dictionary_full: information_schema + pg_description +
  data_dictionary + data_domain 4-way LEFT JOIN
  → ga 스키마 모든 컬럼 자동 노출 (사전 미등록 컬럼도 회색 표시)
- 도메인 30개 초기 데이터 (IDENTIFIER 7 / PII 6 / MONEY 5 / RATE 3 /
  DATE 4 / CODE 4 / TEXT 3 / JSON 2)
- 사전 매핑 50개 (agent 10 / contract 7 / recruit_ledger 9 /
  settle_master 9 / payment 6)
- 메뉴 SYSTEM_DATA_DOMAIN, SYSTEM_DATA_DICT 등록 + 권한

Backend:
- ga-core VO 9개 + Mapper 2개 + XML 2개
- ga-api Service 2 (도메인 삭제 시 사전 참조 검증) + Controller 2
- 엔드포인트:
  · /api/data-domains (CRUD + /options)
  · /api/data-dictionary (CRUD + /tables + /full?tableName=)

Frontend:
- api/dict.ts (13 endpoints)
- pages/system/DataDomainList.tsx — 카테고리 색상 Tag, 마스킹/암호화 표시,
  ModalForm 등록/수정/삭제
- pages/system/DataDictionary.tsx — 좌(테이블 목록 + prefix 그룹핑) +
  우(v_data_dictionary_full 조회, 사전 미등록 컬럼은 회색+점선 추가 버튼)
- App.tsx + MenuIcon.tsx 매핑 추가

Bug fix:
- DataDomainMapper: WHERE is_active = true → 'Y' (CHAR vs boolean)
- DataDictionaryMapper.selectFromView: ORDER BY sort_order 제거
  (view 에 없는 컬럼)

검증:
- ./gradlew :ga-api:bootJar 통과
- Flyway V20 운영 DB 적용 (17초)
- 4 엔드포인트 모두 200 응답
- agent 테이블 15컬럼 사전 자동 노출 + 도메인 매핑 정상

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-11 00:29:13 +09:00
parent 2136b38e1b
commit cece084631
24 changed files with 2268 additions and 1 deletions
+165
View File
@@ -0,0 +1,165 @@
import api, { PageResponse, unwrap } from './request';
// ─── 타입 ────────────────────────────────────────────────────────────────────
export interface DataDomainResp {
domainCode: string;
domainName: string;
domainCategory: string;
dataType: string;
length?: number;
precision?: number;
scale?: number;
formatPattern?: string;
validationRegex?: string;
exampleValue?: string;
maskingType?: string;
encrypted: string; // 'Y'/'N'
description?: string;
isActive: string;
}
export interface DataDomainOption {
domainCode: string;
domainName: string;
domainCategory: string;
}
export interface DataDomainSaveReq {
domainCode: string;
domainName: string;
domainCategory: string;
dataType: string;
length?: number;
precision?: number;
scale?: number;
formatPattern?: string;
validationRegex?: string;
exampleValue?: string;
maskingType?: string;
encrypted?: string;
description?: string;
isActive?: string;
}
export interface DataDomainQuery {
domainCategory?: string;
isActive?: string;
searchKeyword?: string;
pageNum?: number;
pageSize?: number;
}
export interface DataDictionaryResp {
dictId: number;
tableName: string;
columnName: string;
domainCode?: string;
domainName?: string;
domainCategory?: string;
columnLabel?: string;
description?: string;
isPk: string;
isFk: string;
fkReference?: string;
isRequired: string;
defaultValue?: string;
businessRule?: string;
sampleValue?: string;
maskingType?: string;
encrypted?: string;
}
export interface DataDictionaryFullResp {
tableName: string;
columnName: string;
ordinalPosition: number;
dataType: string;
maxLength?: number;
numPrecision?: number;
numScale?: number;
isNullable: string;
columnDefault?: string;
tableComment?: string;
columnComment?: string;
columnLabel?: string;
dictDescription?: string;
domainCode?: string;
domainName?: string;
domainCategory?: string;
formatPattern?: string;
maskingType?: string;
encrypted?: string;
isPk?: string;
isFk?: string;
fkReference?: string;
businessRule?: string;
sampleValue?: string;
}
export interface DataDictionarySaveReq {
tableName: string;
columnName: string;
columnLabel?: string;
description?: string;
domainCode?: string;
isPk?: string;
isFk?: string;
fkReference?: string;
isRequired?: string;
defaultValue?: string;
businessRule?: string;
sampleValue?: string;
}
export interface DataDictionaryQuery {
tableName?: string;
domainCode?: string;
pageNum?: number;
pageSize?: number;
}
// ─── API ─────────────────────────────────────────────────────────────────────
export const dataDomainApi = {
list: (params: DataDomainQuery) =>
unwrap<PageResponse<DataDomainResp>>(api.get('/api/data-domains', { params })),
options: () =>
unwrap<DataDomainOption[]>(api.get('/api/data-domains/options')),
detail: (domainCode: string) =>
unwrap<DataDomainResp>(api.get(`/api/data-domains/${domainCode}`)),
create: (req: DataDomainSaveReq) =>
unwrap<void>(api.post('/api/data-domains', req)),
update: (domainCode: string, req: DataDomainSaveReq) =>
unwrap<void>(api.put(`/api/data-domains/${domainCode}`, req)),
remove: (domainCode: string) =>
unwrap<void>(api.delete(`/api/data-domains/${domainCode}`)),
};
export const dataDictApi = {
list: (params: DataDictionaryQuery) =>
unwrap<PageResponse<DataDictionaryResp>>(api.get('/api/data-dictionary', { params })),
tables: () =>
unwrap<string[]>(api.get('/api/data-dictionary/tables')),
full: (tableName: string) =>
unwrap<DataDictionaryFullResp[]>(api.get('/api/data-dictionary/full', { params: { tableName } })),
detail: (dictId: number) =>
unwrap<DataDictionaryResp>(api.get(`/api/data-dictionary/${dictId}`)),
create: (req: DataDictionarySaveReq) =>
unwrap<void>(api.post('/api/data-dictionary', req)),
update: (dictId: number, req: DataDictionarySaveReq) =>
unwrap<void>(api.put(`/api/data-dictionary/${dictId}`, req)),
remove: (dictId: number) =>
unwrap<void>(api.delete(`/api/data-dictionary/${dictId}`)),
};