refactor: Improve label toggling functionality in ScreenDesigner and enhance SelectedItemsDetailInputComponent
- Updated the label toggling logic in ScreenDesigner to allow toggling of labels for selected components or all components based on the current selection. - Enhanced the SelectedItemsDetailInputComponent by implementing a caching mechanism for table columns and refining the logic for loading category options based on field groups. - Introduced a new helper function to convert category codes to labels, improving the clarity and maintainability of the price calculation logic. - Added support for determining the source table for field groups, facilitating better data management and retrieval.
This commit is contained in:
parent
79d8f0b160
commit
bb4d90fd58
|
|
@ -1871,17 +1871,33 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
[groupState.selectedComponents, layout, selectedComponent?.id, saveToHistory]
|
||||
);
|
||||
|
||||
// 라벨 일괄 토글
|
||||
// 라벨 일괄 토글 (선택된 컴포넌트가 있으면 선택된 것만, 없으면 전체)
|
||||
const handleToggleAllLabels = useCallback(() => {
|
||||
saveToHistory(layout);
|
||||
const newComponents = toggleAllLabels(layout.components);
|
||||
|
||||
const selectedIds = groupState.selectedComponents;
|
||||
const isPartial = selectedIds.length > 0;
|
||||
|
||||
// 토글 대상 컴포넌트 필터
|
||||
const targetComponents = layout.components.filter((c) => {
|
||||
if (!c.label || ["group", "datatable"].includes(c.type)) return false;
|
||||
if (isPartial) return selectedIds.includes(c.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
const hadHidden = targetComponents.some(
|
||||
(c) => (c.style as any)?.labelDisplay === false
|
||||
);
|
||||
|
||||
const newComponents = toggleAllLabels(layout.components, selectedIds);
|
||||
setLayout((prev) => ({ ...prev, components: newComponents }));
|
||||
|
||||
const hasHidden = layout.components.some(
|
||||
(c) => c.type === "widget" && (c.style as any)?.labelDisplay === false
|
||||
);
|
||||
toast.success(hasHidden ? "모든 라벨 표시" : "모든 라벨 숨기기");
|
||||
}, [layout, saveToHistory]);
|
||||
// 강제 리렌더링 트리거
|
||||
setForceRenderTrigger((prev) => prev + 1);
|
||||
|
||||
const scope = isPartial ? `선택된 ${targetComponents.length}개` : "모든";
|
||||
toast.success(hadHidden ? `${scope} 라벨 표시` : `${scope} 라벨 숨기기`);
|
||||
}, [layout, saveToHistory, groupState.selectedComponents]);
|
||||
|
||||
// Nudge (화살표 키 이동)
|
||||
const handleNudge = useCallback(
|
||||
|
|
|
|||
|
|
@ -146,59 +146,69 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
const newOptions: Record<string, Array<{ label: string; value: string }>> = { ...codeOptions };
|
||||
|
||||
// 🆕 대상 테이블의 컬럼 메타데이터에서 codeCategory 가져오기
|
||||
const targetTable = componentConfig.targetTable;
|
||||
let targetTableColumns: any[] = [];
|
||||
// 🆕 그룹별 sourceTable 매핑 구성
|
||||
const groups = componentConfig.fieldGroups || [];
|
||||
const groupSourceTableMap: Record<string, string> = {};
|
||||
groups.forEach((g) => {
|
||||
if (g.sourceTable) {
|
||||
groupSourceTableMap[g.id] = g.sourceTable;
|
||||
}
|
||||
});
|
||||
const defaultTargetTable = componentConfig.targetTable;
|
||||
|
||||
if (targetTable) {
|
||||
// 테이블별 컬럼 메타데이터 캐시
|
||||
const tableColumnsCache: Record<string, any[]> = {};
|
||||
const getTableColumns = async (tableName: string) => {
|
||||
if (tableColumnsCache[tableName]) return tableColumnsCache[tableName];
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const columnsResponse = await tableTypeApi.getColumns(targetTable);
|
||||
targetTableColumns = columnsResponse || [];
|
||||
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
||||
tableColumnsCache[tableName] = columnsResponse || [];
|
||||
return tableColumnsCache[tableName];
|
||||
} catch (error) {
|
||||
console.error("❌ 대상 테이블 컬럼 조회 실패:", error);
|
||||
console.error(`❌ 테이블 컬럼 조회 실패 (${tableName}):`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const field of codeFields) {
|
||||
// 이미 로드된 옵션이면 스킵
|
||||
if (newOptions[field.name]) {
|
||||
console.log(`⏭️ 이미 로드된 옵션 (${field.name})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 🆕 필드의 그룹 sourceTable 결정
|
||||
const fieldSourceTable = (field.groupId && groupSourceTableMap[field.groupId]) || defaultTargetTable;
|
||||
|
||||
try {
|
||||
// 🆕 category 타입이면 table_column_category_values에서 로드
|
||||
if (field.inputType === "category" && targetTable) {
|
||||
console.log(`🔄 카테고리 옵션 로드 시도 (${targetTable}.${field.name})`);
|
||||
if (field.inputType === "category" && fieldSourceTable) {
|
||||
console.log(`🔄 카테고리 옵션 로드 (${fieldSourceTable}.${field.name})`);
|
||||
|
||||
const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
|
||||
const response = await getCategoryValues(targetTable, field.name, false);
|
||||
const response = await getCategoryValues(fieldSourceTable, field.name, false);
|
||||
|
||||
console.log("📥 getCategoryValues 응답:", response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
if (response.success && response.data && response.data.length > 0) {
|
||||
newOptions[field.name] = response.data.map((item: any) => ({
|
||||
label: item.value_label || item.valueLabel,
|
||||
value: item.value_code || item.valueCode,
|
||||
}));
|
||||
console.log(`✅ 카테고리 옵션 로드 완료 (${field.name}):`, newOptions[field.name]);
|
||||
console.log(`✅ 카테고리 옵션 로드 완료 (${fieldSourceTable}.${field.name}):`, newOptions[field.name]);
|
||||
} else {
|
||||
console.error(`❌ 카테고리 옵션 로드 실패 (${field.name}):`, response.error || "응답 없음");
|
||||
console.warn(`⚠️ 카테고리 옵션 없음 (${fieldSourceTable}.${field.name})`);
|
||||
}
|
||||
} else if (field.inputType === "code") {
|
||||
// code 타입이면 기존대로 code_info에서 로드
|
||||
// 이미 codeCategory가 있으면 사용
|
||||
let codeCategory = field.codeCategory;
|
||||
|
||||
// 🆕 codeCategory가 없으면 대상 테이블 컬럼에서 찾기
|
||||
if (!codeCategory && targetTableColumns.length > 0) {
|
||||
const columnMeta = targetTableColumns.find(
|
||||
(col: any) => (col.columnName || col.column_name) === field.name,
|
||||
);
|
||||
if (columnMeta) {
|
||||
codeCategory = columnMeta.codeCategory || columnMeta.code_category;
|
||||
console.log(`🔍 필드 "${field.name}"의 codeCategory를 메타데이터에서 찾음:`, codeCategory);
|
||||
if (!codeCategory && fieldSourceTable) {
|
||||
const targetTableColumns = await getTableColumns(fieldSourceTable);
|
||||
if (targetTableColumns.length > 0) {
|
||||
const columnMeta = targetTableColumns.find(
|
||||
(col: any) => (col.columnName || col.column_name) === field.name,
|
||||
);
|
||||
if (columnMeta) {
|
||||
codeCategory = columnMeta.codeCategory || columnMeta.code_category;
|
||||
console.log(`🔍 필드 "${field.name}"의 codeCategory를 메타데이터에서 찾음:`, codeCategory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -784,13 +794,22 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
onClick?.();
|
||||
};
|
||||
|
||||
// 🆕 실시간 단가 계산 함수 (설정 기반 + 카테고리 매핑)
|
||||
// 🆕 카테고리 코드 → 라벨명 변환 헬퍼
|
||||
const getOptionLabel = useCallback(
|
||||
(fieldName: string, valueCode: string): string => {
|
||||
const options = codeOptions[fieldName] || [];
|
||||
const matched = options.find((opt) => opt.value === valueCode);
|
||||
return matched?.label || valueCode || "";
|
||||
},
|
||||
[codeOptions],
|
||||
);
|
||||
|
||||
// 🆕 실시간 단가 계산 함수 (라벨명 기반 - 회사별 코드 무관)
|
||||
const calculatePrice = useCallback(
|
||||
(entry: GroupEntry): number => {
|
||||
// 자동 계산 설정이 없으면 계산하지 않음
|
||||
if (!componentConfig.autoCalculation) return 0;
|
||||
|
||||
const { inputFields, valueMapping } = componentConfig.autoCalculation;
|
||||
const { inputFields } = componentConfig.autoCalculation;
|
||||
|
||||
// 기본 단가
|
||||
const basePrice = parseFloat(entry[inputFields.basePrice] || "0");
|
||||
|
|
@ -798,38 +817,46 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
let price = basePrice;
|
||||
|
||||
// 1단계: 할인 적용
|
||||
const discountTypeValue = entry[inputFields.discountType];
|
||||
// 1단계: 할인 적용 (라벨명으로 판단)
|
||||
const discountTypeCode = entry[inputFields.discountType];
|
||||
const discountTypeLabel = getOptionLabel("discount_type", discountTypeCode);
|
||||
const discountValue = parseFloat(entry[inputFields.discountValue] || "0");
|
||||
|
||||
// 매핑을 통해 실제 연산 타입 결정
|
||||
const discountOperation = valueMapping?.discountType?.[discountTypeValue] || "none";
|
||||
|
||||
if (discountOperation === "rate") {
|
||||
if (discountTypeLabel.includes("할인율") || discountTypeLabel.includes("%")) {
|
||||
// 할인율(%)
|
||||
price = price * (1 - discountValue / 100);
|
||||
} else if (discountOperation === "amount") {
|
||||
} else if (discountTypeLabel.includes("할인금액") || discountTypeLabel.includes("금액")) {
|
||||
// 할인금액
|
||||
price = price - discountValue;
|
||||
}
|
||||
// "할인없음"이면 그대로
|
||||
|
||||
// 2단계: 반올림 적용
|
||||
const roundingTypeValue = entry[inputFields.roundingType];
|
||||
const roundingUnitValue = entry[inputFields.roundingUnit];
|
||||
// rounding_type = 단위 (10원, 100원, 1000원)
|
||||
// rounding_unit_value = 방법 (반올림, 절삭, 올림, 반올림없음)
|
||||
const roundingTypeCode = entry[inputFields.roundingType];
|
||||
const roundingTypeLabel = getOptionLabel("rounding_type", roundingTypeCode);
|
||||
|
||||
// 매핑을 통해 실제 연산 타입 결정
|
||||
const roundingOperation = valueMapping?.roundingType?.[roundingTypeValue] || "none";
|
||||
const unit = valueMapping?.roundingUnit?.[roundingUnitValue] || parseFloat(roundingUnitValue) || 1;
|
||||
const roundingUnitCode = entry[inputFields.roundingUnit];
|
||||
const roundingUnitLabel = getOptionLabel("rounding_unit_value", roundingUnitCode);
|
||||
|
||||
if (roundingOperation === "round") {
|
||||
// roundingType 라벨에서 단위 숫자 추출 (예: "10원" → 10, "1000원" → 1000)
|
||||
const unitMatch = roundingTypeLabel.match(/(\d+)/);
|
||||
const unit = unitMatch ? parseInt(unitMatch[1]) : parseFloat(roundingTypeCode) || 1;
|
||||
|
||||
// roundingUnit 라벨로 반올림 방법 결정
|
||||
if (roundingUnitLabel.includes("반올림") && !roundingUnitLabel.includes("없음")) {
|
||||
price = Math.round(price / unit) * unit;
|
||||
} else if (roundingOperation === "floor") {
|
||||
} else if (roundingUnitLabel.includes("절삭")) {
|
||||
price = Math.floor(price / unit) * unit;
|
||||
} else if (roundingOperation === "ceil") {
|
||||
} else if (roundingUnitLabel.includes("올림")) {
|
||||
price = Math.ceil(price / unit) * unit;
|
||||
}
|
||||
// "반올림없음"이면 그대로
|
||||
|
||||
return price;
|
||||
},
|
||||
[componentConfig.autoCalculation],
|
||||
[componentConfig.autoCalculation, getOptionLabel],
|
||||
);
|
||||
|
||||
// 🆕 그룹별 필드 변경 핸들러: itemId + groupId + entryId + fieldName
|
||||
|
|
@ -851,7 +878,23 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
const existingEntryIndex = groupEntries.findIndex((e) => e.id === entryId);
|
||||
|
||||
if (existingEntryIndex >= 0) {
|
||||
// 기존 entry 업데이트 (항상 이 경로로만 진입)
|
||||
const currentEntry = groupEntries[existingEntryIndex];
|
||||
|
||||
// 날짜 검증: 종료일이 시작일보다 앞서면 차단
|
||||
if (fieldName === "end_date" && value && currentEntry.start_date) {
|
||||
if (new Date(value) < new Date(currentEntry.start_date as string)) {
|
||||
alert("종료일은 시작일보다 이후여야 합니다.");
|
||||
return item; // 변경 취소
|
||||
}
|
||||
}
|
||||
if (fieldName === "start_date" && value && currentEntry.end_date) {
|
||||
if (new Date(value) > new Date(currentEntry.end_date as string)) {
|
||||
alert("시작일은 종료일보다 이전이어야 합니다.");
|
||||
return item; // 변경 취소
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 entry 업데이트
|
||||
const updatedEntries = [...groupEntries];
|
||||
const updatedEntry = {
|
||||
...updatedEntries[existingEntryIndex],
|
||||
|
|
@ -1099,16 +1142,19 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
case "integer":
|
||||
case "bigint":
|
||||
case "decimal":
|
||||
case "numeric":
|
||||
// 🆕 계산된 단가는 천 단위 구분 및 강조 표시
|
||||
if (isCalculatedField) {
|
||||
const numericValue = parseFloat(value) || 0;
|
||||
const formattedValue = new Intl.NumberFormat("ko-KR").format(numericValue);
|
||||
case "numeric": {
|
||||
// 숫자 포맷팅 헬퍼: 콤마 표시 + 실제 값은 숫자만 저장
|
||||
const rawNum = value ? String(value).replace(/,/g, "") : "";
|
||||
const displayNum = rawNum && !isNaN(Number(rawNum))
|
||||
? new Intl.NumberFormat("ko-KR").format(Number(rawNum))
|
||||
: rawNum;
|
||||
|
||||
// 계산된 단가는 읽기 전용 + 강조 표시
|
||||
if (isCalculatedField) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={formattedValue}
|
||||
value={displayNum}
|
||||
readOnly
|
||||
disabled
|
||||
className={cn(
|
||||
|
|
@ -1124,14 +1170,20 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="number"
|
||||
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
|
||||
min={field.validation?.min}
|
||||
max={field.validation?.max}
|
||||
value={displayNum}
|
||||
placeholder={field.placeholder}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
onChange={(e) => {
|
||||
// 콤마 제거 후 숫자만 저장
|
||||
const cleaned = e.target.value.replace(/,/g, "").replace(/[^0-9.\-]/g, "");
|
||||
handleFieldChange(itemId, groupId, entryId, field.name, cleaned);
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "date":
|
||||
case "timestamp":
|
||||
|
|
@ -1194,7 +1246,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
onValueChange={(val) => handleFieldChange(itemId, groupId, entryId, field.name, val)}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
>
|
||||
<SelectTrigger size="default" className="h-7 w-full text-xs">
|
||||
<SelectTrigger className="h-7 w-full text-xs">
|
||||
<SelectValue placeholder={field.placeholder || "선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -1220,7 +1272,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
{...commonProps}
|
||||
type="text"
|
||||
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1231,7 +1283,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
onValueChange={(val) => handleFieldChange(itemId, groupId, entryId, field.name, val)}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs sm:h-10 sm:text-sm">
|
||||
<SelectTrigger className="h-7 w-full text-xs">
|
||||
<SelectValue placeholder={field.placeholder || "선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -1254,7 +1306,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
type="text"
|
||||
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
|
||||
maxLength={field.validation?.maxLength}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1295,42 +1347,91 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
const displayItems = group?.displayItems || [];
|
||||
|
||||
if (displayItems.length === 0) {
|
||||
// displayItems가 없으면 기본 방식 (해당 그룹의 visible 필드만 나열)
|
||||
const fields = (componentConfig.additionalFields || []).filter((f) => {
|
||||
// 그룹 필터
|
||||
const matchGroup = componentConfig.fieldGroups && componentConfig.fieldGroups.length > 0
|
||||
? f.groupId === groupId
|
||||
: true;
|
||||
// hidden 필드 제외 (width: "0px"인 필드)
|
||||
const isVisible = f.width !== "0px";
|
||||
return matchGroup && isVisible;
|
||||
});
|
||||
|
||||
// 값이 있는 필드만 "라벨: 값" 형식으로 표시
|
||||
const displayParts = fields
|
||||
.map((f) => {
|
||||
const value = entry[f.name];
|
||||
if (!value && value !== 0) return null;
|
||||
// 헬퍼: 값을 사람이 읽기 좋은 형태로 변환
|
||||
const formatValue = (f: any, value: any): string => {
|
||||
if (!value && value !== 0) return "";
|
||||
const strValue = String(value);
|
||||
|
||||
const strValue = String(value);
|
||||
// 날짜 포맷
|
||||
const isoDateMatch = strValue.match(/^(\d{4})-(\d{2})-(\d{2})(T|\s|$)/);
|
||||
if (isoDateMatch) {
|
||||
const [, year, month, day] = isoDateMatch;
|
||||
return `${year}.${month}.${day}`;
|
||||
}
|
||||
|
||||
// ISO 날짜 형식 자동 포맷팅
|
||||
const isoDateMatch = strValue.match(/^(\d{4})-(\d{2})-(\d{2})(T|\s|$)/);
|
||||
if (isoDateMatch) {
|
||||
const [, year, month, day] = isoDateMatch;
|
||||
return `${f.label}: ${year}.${month}.${day}`;
|
||||
}
|
||||
// 카테고리/코드 -> 라벨명
|
||||
const renderType = f.inputType || f.type;
|
||||
if (renderType === "category" || renderType === "code" || renderType === "select") {
|
||||
const options = codeOptions[f.name] || f.options || [];
|
||||
const matched = options.find((opt: any) => opt.value === strValue);
|
||||
if (matched) return matched.label;
|
||||
}
|
||||
|
||||
return `${f.label}: ${strValue}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
// 숫자는 천 단위 구분
|
||||
if (renderType === "number" && !isNaN(Number(strValue))) {
|
||||
return new Intl.NumberFormat("ko-KR").format(Number(strValue));
|
||||
}
|
||||
|
||||
// 빈 항목 표시: 그룹 필드명으로 안내 메시지 생성
|
||||
if (displayParts.length === 0) {
|
||||
return strValue;
|
||||
};
|
||||
|
||||
// 간결한 요약 생성 (그룹별 핵심 정보만)
|
||||
const hasAnyValue = fields.some((f) => {
|
||||
const v = entry[f.name];
|
||||
return v !== undefined && v !== null && v !== "";
|
||||
});
|
||||
|
||||
if (!hasAnyValue) {
|
||||
const fieldLabels = fields.slice(0, 2).map(f => f.label).join("/");
|
||||
return `신규 ${fieldLabels} 입력`;
|
||||
}
|
||||
return displayParts.join(" ");
|
||||
|
||||
// 날짜 범위가 있으면 우선 표시
|
||||
const startDate = entry["start_date"] ? formatValue({ inputType: "date" }, entry["start_date"]) : "";
|
||||
const endDate = entry["end_date"] ? formatValue({ inputType: "date" }, entry["end_date"]) : "";
|
||||
|
||||
// 기준단가(calculated_price) 또는 기준가(base_price) 표시
|
||||
const calcPrice = entry["calculated_price"] ? formatValue({ inputType: "number" }, entry["calculated_price"]) : "";
|
||||
const basePrice = entry["base_price"] ? formatValue({ inputType: "number" }, entry["base_price"]) : "";
|
||||
|
||||
// 통화코드
|
||||
const currencyCode = entry["currency_code"] ? formatValue(
|
||||
fields.find(f => f.name === "currency_code") || { inputType: "category", name: "currency_code" },
|
||||
entry["currency_code"]
|
||||
) : "";
|
||||
|
||||
if (startDate || calcPrice || basePrice) {
|
||||
// 날짜 + 단가 간결 표시
|
||||
const parts: string[] = [];
|
||||
if (startDate) {
|
||||
parts.push(endDate ? `${startDate} ~ ${endDate}` : `${startDate} ~`);
|
||||
}
|
||||
if (calcPrice) {
|
||||
parts.push(`${currencyCode || ""} ${calcPrice}`.trim());
|
||||
} else if (basePrice) {
|
||||
parts.push(`${currencyCode || ""} ${basePrice}`.trim());
|
||||
}
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
// 그 외 그룹 (거래처 품번 등): 첫 2개 필드만 표시
|
||||
const summaryParts = fields
|
||||
.slice(0, 3)
|
||||
.map((f) => {
|
||||
const value = entry[f.name];
|
||||
if (!value && value !== 0) return null;
|
||||
return `${f.label}: ${formatValue(f, value)}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return summaryParts.join(" ");
|
||||
}
|
||||
|
||||
// displayItems 설정대로 렌더링
|
||||
|
|
@ -1499,7 +1600,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
</>
|
||||
);
|
||||
},
|
||||
[componentConfig.fieldGroups, componentConfig.additionalFields],
|
||||
[componentConfig.fieldGroups, componentConfig.additionalFields, codeOptions],
|
||||
);
|
||||
|
||||
// 빈 상태 렌더링
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export interface FieldGroup {
|
|||
order?: number;
|
||||
/** 🆕 최대 항목 수 (1이면 1:1 관계 - 자동 생성, + 추가 버튼 숨김) */
|
||||
maxEntries?: number;
|
||||
/** 🆕 이 그룹의 소스 테이블 (카테고리 옵션 로드 시 사용) */
|
||||
sourceTable?: string;
|
||||
/** 🆕 이 그룹의 항목 표시 설정 (그룹별로 다른 표시 형식 가능) */
|
||||
displayItems?: DisplayItem[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,19 +214,53 @@ export function matchComponentSize(
|
|||
* 모든 컴포넌트의 라벨 표시/숨기기를 토글합니다.
|
||||
* 숨겨진 라벨이 하나라도 있으면 모두 표시, 모두 표시되어 있으면 모두 숨기기
|
||||
*/
|
||||
export function toggleAllLabels(components: ComponentData[], forceShow?: boolean): ComponentData[] {
|
||||
// 현재 라벨이 숨겨진(labelDisplay === false) 컴포넌트가 있는지 확인
|
||||
const hasHiddenLabel = components.some(
|
||||
(c) => c.type === "widget" && (c.style as any)?.labelDisplay === false
|
||||
/**
|
||||
* 라벨 토글 대상 타입 판별
|
||||
* label 속성이 있고, style.labelDisplay를 지원하는 컴포넌트인지 확인
|
||||
*/
|
||||
function hasLabelSupport(component: ComponentData): boolean {
|
||||
// 라벨이 없는 컴포넌트는 제외
|
||||
if (!component.label) return false;
|
||||
|
||||
// 그룹, datatable 등은 라벨 토글 대상에서 제외
|
||||
const excludedTypes = ["group", "datatable"];
|
||||
if (excludedTypes.includes(component.type)) return false;
|
||||
|
||||
// 나머지 (widget, component, container, file, flow 등)는 대상
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param components - 전체 컴포넌트 배열
|
||||
* @param selectedIds - 선택된 컴포넌트 ID 목록 (빈 배열이면 전체 대상)
|
||||
* @param forceShow - 강제 표시/숨기기 (지정하지 않으면 자동 토글)
|
||||
*/
|
||||
export function toggleAllLabels(
|
||||
components: ComponentData[],
|
||||
selectedIds: string[] = [],
|
||||
forceShow?: boolean
|
||||
): ComponentData[] {
|
||||
// 대상 컴포넌트 필터: selectedIds가 있으면 선택된 것만, 없으면 전체
|
||||
const targetComponents = components.filter((c) => {
|
||||
if (!hasLabelSupport(c)) return false;
|
||||
if (selectedIds.length > 0) return selectedIds.includes(c.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 대상 중 라벨이 숨겨진 컴포넌트가 있는지 확인
|
||||
const hasHiddenLabel = targetComponents.some(
|
||||
(c) => (c.style as any)?.labelDisplay === false
|
||||
);
|
||||
|
||||
// forceShow가 지정되면 그 값 사용, 아니면 자동 판단
|
||||
// 숨겨진 라벨이 있으면 모두 표시, 아니면 모두 숨기기
|
||||
const shouldShow = forceShow !== undefined ? forceShow : hasHiddenLabel;
|
||||
|
||||
// 대상 ID Set (빠른 조회용)
|
||||
const targetIdSet = new Set(targetComponents.map((c) => c.id));
|
||||
|
||||
return components.map((c) => {
|
||||
// 위젯 타입만 라벨 토글 대상
|
||||
if (c.type !== "widget") return c;
|
||||
// 대상이 아닌 컴포넌트는 건드리지 않음
|
||||
if (!targetIdSet.has(c.id)) return c;
|
||||
|
||||
return {
|
||||
...c,
|
||||
|
|
|
|||
Loading…
Reference in New Issue