feat(modal-repeater-table): 컬럼 매핑 및 계산 규칙 UI 대폭 개선

새로운 기능
- 컬럼별 독립적인 소스 테이블 선택 기능
- SourceColumnSelector, ReferenceColumnSelector 컴포넌트 추가
- 계산 규칙 자동 동기화 로직 (cleanupInitialConfig)

UI/UX 개선
- 컬럼 설정 UI를 세로 레이아웃으로 재구성 (h-10 통일)
- 매핑 타입별 색상 구분 (파란색/보라색/초록색)
- 계산 규칙 섹션 재디자인 (안내 박스, 번호 배지, 빈 상태)
- 현재 설정 시각화 (코드 스타일 표시)

버그 수정
- 계산 규칙 삭제 시 컬럼이 수정 불가능 상태로 남는 문제 해결
- 결과 필드 변경 시 이전 필드의 calculated 속성 제거
- 초기 로드 시 계산 규칙과 컬럼 속성 동기화

개선 사항
- 모든 입력 필드의 높이와 텍스트 크기 일관성 확보
- 섹션별 명확한 제목과 설명 추가
- 접근성 향상 (ARIA 레이블, 포커스 스타일)
This commit is contained in:
SeongHyun Kim
2025-11-19 17:09:12 +09:00
parent 8eccdd0b4c
commit d5d267e63a
4 changed files with 1119 additions and 222 deletions

View File

@@ -128,20 +128,56 @@ export function ModalRepeaterTableComponent({
const handleAddItems = (items: any[]) => {
console.log(" handleAddItems 호출:", items.length, "개 항목");
console.log("📋 소스 데이터:", items);
// 매핑 규칙에 따라 데이터 변환
const mappedItems = items.map((sourceItem) => {
const newItem: any = {};
// 기본값 적용
const itemsWithDefaults = items.map((item) => {
const newItem = { ...item };
columns.forEach((col) => {
console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping);
// 1. 매핑 규칙이 있는 경우
if (col.mapping) {
if (col.mapping.type === "source") {
// 소스 테이블 컬럼에서 복사
const sourceField = col.mapping.sourceField;
if (sourceField && sourceItem[sourceField] !== undefined) {
newItem[col.field] = sourceItem[sourceField];
console.log(` ✅ 소스 복사: ${sourceField}${col.field}:`, newItem[col.field]);
} else {
console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`);
}
} else if (col.mapping.type === "reference") {
// 외부 테이블 참조 (TODO: API 호출 필요)
console.log(` ⏳ 참조 조회 필요: ${col.mapping.referenceTable}.${col.mapping.referenceField}`);
// 현재는 빈 값으로 설정 (나중에 API 호출로 구현)
newItem[col.field] = undefined;
} else if (col.mapping.type === "manual") {
// 사용자 입력 (빈 값)
newItem[col.field] = undefined;
console.log(` ✏️ 수동 입력 필드`);
}
}
// 2. 매핑 규칙이 없는 경우 - 소스 데이터에서 같은 필드명으로 복사
else if (sourceItem[col.field] !== undefined) {
newItem[col.field] = sourceItem[col.field];
console.log(` 📝 직접 복사: ${col.field}:`, newItem[col.field]);
}
// 3. 기본값 적용
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
newItem[col.field] = col.defaultValue;
console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue);
}
});
console.log("📦 변환된 항목:", newItem);
return newItem;
});
// 계산 필드 업데이트
const calculatedItems = calculateAll(itemsWithDefaults);
const calculatedItems = calculateAll(mappedItems);
// 기존 데이터에 추가
const newData = [...value, ...calculatedItems];

View File

@@ -9,6 +9,9 @@ export interface ModalRepeaterTableProps {
sourceColumns: string[]; // 모달에 표시할 컬럼들
sourceSearchFields?: string[]; // 검색 가능한 필드들
// 🆕 저장 대상 테이블 설정
targetTable?: string; // 저장할 테이블 (예: "sales_order_mng")
// 모달 설정
modalTitle: string; // 모달 제목 (예: "품목 검색 및 선택")
modalButtonText?: string; // 모달 열기 버튼 텍스트 (기본: "품목 검색")
@@ -45,6 +48,41 @@ export interface RepeaterColumnConfig {
required?: boolean; // 필수 입력 여부
defaultValue?: any; // 기본값
selectOptions?: { value: string; label: string }[]; // select일 때 옵션
// 🆕 컬럼 매핑 설정
mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정
}
/**
* 컬럼 매핑 설정
* 반복 테이블 컬럼이 어느 테이블의 어느 컬럼에서 값을 가져올지 정의
*/
export interface ColumnMapping {
/** 매핑 타입 */
type: "source" | "reference" | "manual";
/** 매핑 타입별 설정 */
// type: "source" - 소스 테이블 (모달에서 선택한 항목)의 컬럼에서 가져오기
sourceField?: string; // 소스 테이블의 컬럼명 (예: "item_name")
// type: "reference" - 외부 테이블 참조 (조인)
referenceTable?: string; // 참조 테이블명 (예: "customer_item_mapping")
referenceField?: string; // 참조 테이블에서 가져올 컬럼 (예: "basic_price")
joinCondition?: JoinCondition[]; // 조인 조건
// type: "manual" - 사용자가 직접 입력
}
/**
* 조인 조건 정의
*/
export interface JoinCondition {
/** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */
sourceField: string;
/** 참조 테이블의 컬럼 */
targetField: string;
/** 비교 연산자 */
operator?: "=" | "!=" | ">" | "<" | ">=" | "<=";
}
export interface CalculationRule {

View File

@@ -16,15 +16,26 @@ export function useCalculation(calculationRules: CalculationRule[] = []) {
for (const rule of calculationRules) {
try {
// formula에서 필드명 추출 및 값으로 대체
// formula에서 필드명 자동 추출 (영문자, 숫자, 언더스코어로 구성된 단어)
let formula = rule.formula;
const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
// 추출된 필드명들을 사용 (dependencies가 없으면 자동 추출 사용)
const dependencies = rule.dependencies && rule.dependencies.length > 0
? rule.dependencies
: fieldMatches;
for (const dep of rule.dependencies) {
// 필드명을 실제 값으로 대체
for (const dep of dependencies) {
// 결과 필드는 제외
if (dep === rule.result) continue;
const value = parseFloat(row[dep]) || 0;
formula = formula.replace(new RegExp(dep, "g"), value.toString());
// 정확한 필드명만 대체 (단어 경계 사용)
formula = formula.replace(new RegExp(`\\b${dep}\\b`, "g"), value.toString());
}
// 계산 실행 (eval 대신 Function 사용)
// 계산 실행 (Function 사용)
const result = new Function(`return ${formula}`)();
updatedRow[rule.result] = result;
} catch (error) {