조건부 계산식
This commit is contained in:
parent
417e1d297b
commit
eb868965df
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
|
@ -84,6 +84,9 @@ export function RepeaterTable({
|
|||
onSelectionChange,
|
||||
equalizeWidthsTrigger,
|
||||
}: RepeaterTableProps) {
|
||||
// 히든 컬럼을 제외한 표시 가능한 컬럼만 필터링
|
||||
const visibleColumns = useMemo(() => columns.filter((col) => !col.hidden), [columns]);
|
||||
|
||||
// 컨테이너 ref - 실제 너비 측정용
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -145,7 +148,7 @@ export function RepeaterTable({
|
|||
// 컬럼 너비 상태 관리
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() => {
|
||||
const widths: Record<string, number> = {};
|
||||
columns.forEach((col) => {
|
||||
columns.filter((col) => !col.hidden).forEach((col) => {
|
||||
widths[col.field] = col.width ? parseInt(col.width) : 120;
|
||||
});
|
||||
return widths;
|
||||
|
|
@ -154,11 +157,11 @@ export function RepeaterTable({
|
|||
// 기본 너비 저장 (리셋용)
|
||||
const defaultWidths = React.useMemo(() => {
|
||||
const widths: Record<string, number> = {};
|
||||
columns.forEach((col) => {
|
||||
visibleColumns.forEach((col) => {
|
||||
widths[col.field] = col.width ? parseInt(col.width) : 120;
|
||||
});
|
||||
return widths;
|
||||
}, [columns]);
|
||||
}, [visibleColumns]);
|
||||
|
||||
// 리사이즈 상태
|
||||
const [resizing, setResizing] = useState<{ field: string; startX: number; startWidth: number } | null>(null);
|
||||
|
|
@ -206,7 +209,7 @@ export function RepeaterTable({
|
|||
// 해당 컬럼의 가장 긴 글자 너비 계산
|
||||
// equalWidth: 균등 분배 시 너비 (값이 없는 컬럼의 최소값으로 사용)
|
||||
const calculateColumnContentWidth = (field: string, equalWidth: number): number => {
|
||||
const column = columns.find((col) => col.field === field);
|
||||
const column = visibleColumns.find((col) => col.field === field);
|
||||
if (!column) return equalWidth;
|
||||
|
||||
// 날짜 필드는 110px (yyyy-MM-dd)
|
||||
|
|
@ -257,7 +260,7 @@ export function RepeaterTable({
|
|||
// 헤더 더블클릭: 해당 컬럼만 글자 너비에 맞춤
|
||||
const handleDoubleClick = (field: string) => {
|
||||
const availableWidth = getAvailableWidth();
|
||||
const equalWidth = Math.max(60, Math.floor(availableWidth / columns.length));
|
||||
const equalWidth = Math.max(60, Math.floor(availableWidth / visibleColumns.length));
|
||||
const contentWidth = calculateColumnContentWidth(field, equalWidth);
|
||||
setColumnWidths((prev) => ({
|
||||
...prev,
|
||||
|
|
@ -268,10 +271,10 @@ export function RepeaterTable({
|
|||
// 균등 분배: 컬럼 수로 테이블 너비를 균등 분배
|
||||
const applyEqualizeWidths = () => {
|
||||
const availableWidth = getAvailableWidth();
|
||||
const equalWidth = Math.max(60, Math.floor(availableWidth / columns.length));
|
||||
const equalWidth = Math.max(60, Math.floor(availableWidth / visibleColumns.length));
|
||||
|
||||
const newWidths: Record<string, number> = {};
|
||||
columns.forEach((col) => {
|
||||
visibleColumns.forEach((col) => {
|
||||
newWidths[col.field] = equalWidth;
|
||||
});
|
||||
|
||||
|
|
@ -280,15 +283,15 @@ export function RepeaterTable({
|
|||
|
||||
// 자동 맞춤: 각 컬럼을 글자 너비에 맞추고, 컨테이너보다 작으면 남는 공간 분배
|
||||
const applyAutoFitWidths = () => {
|
||||
if (columns.length === 0) return;
|
||||
if (visibleColumns.length === 0) return;
|
||||
|
||||
// 균등 분배 너비 계산 (값이 없는 컬럼의 최소값)
|
||||
const availableWidth = getAvailableWidth();
|
||||
const equalWidth = Math.max(60, Math.floor(availableWidth / columns.length));
|
||||
const equalWidth = Math.max(60, Math.floor(availableWidth / visibleColumns.length));
|
||||
|
||||
// 1. 각 컬럼의 글자 너비 계산 (값이 없으면 균등 분배 너비 사용)
|
||||
const newWidths: Record<string, number> = {};
|
||||
columns.forEach((col) => {
|
||||
visibleColumns.forEach((col) => {
|
||||
newWidths[col.field] = calculateColumnContentWidth(col.field, equalWidth);
|
||||
});
|
||||
|
||||
|
|
@ -298,8 +301,8 @@ export function RepeaterTable({
|
|||
// 3. 컨테이너보다 작으면 남는 공간을 균등 분배 (테이블 꽉 참 유지)
|
||||
if (totalContentWidth < availableWidth) {
|
||||
const extraSpace = availableWidth - totalContentWidth;
|
||||
const extraPerColumn = Math.floor(extraSpace / columns.length);
|
||||
columns.forEach((col) => {
|
||||
const extraPerColumn = Math.floor(extraSpace / visibleColumns.length);
|
||||
visibleColumns.forEach((col) => {
|
||||
newWidths[col.field] += extraPerColumn;
|
||||
});
|
||||
}
|
||||
|
|
@ -311,7 +314,7 @@ export function RepeaterTable({
|
|||
// 초기 마운트 시 균등 분배 적용
|
||||
useEffect(() => {
|
||||
if (initializedRef.current) return;
|
||||
if (!containerRef.current || columns.length === 0) return;
|
||||
if (!containerRef.current || visibleColumns.length === 0) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
applyEqualizeWidths();
|
||||
|
|
@ -319,7 +322,7 @@ export function RepeaterTable({
|
|||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [columns]);
|
||||
}, [visibleColumns]);
|
||||
|
||||
// 트리거 감지: 1=균등분배, 2=자동맞춤
|
||||
useEffect(() => {
|
||||
|
|
@ -357,7 +360,7 @@ export function RepeaterTable({
|
|||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [resizing, columns, data]);
|
||||
}, [resizing, visibleColumns, data]);
|
||||
|
||||
// 데이터 변경 감지 (필요시 활성화)
|
||||
// useEffect(() => {
|
||||
|
|
@ -531,7 +534,7 @@ export function RepeaterTable({
|
|||
className={cn("border-gray-400", isIndeterminate && "data-[state=checked]:bg-primary")}
|
||||
/>
|
||||
</th>
|
||||
{columns.map((col) => {
|
||||
{visibleColumns.map((col) => {
|
||||
const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0;
|
||||
const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId;
|
||||
const activeOption = hasDynamicSource
|
||||
|
|
@ -631,7 +634,7 @@ export function RepeaterTable({
|
|||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length + 2}
|
||||
colSpan={visibleColumns.length + 2}
|
||||
className="border-b border-gray-200 px-4 py-8 text-center text-gray-500"
|
||||
>
|
||||
추가된 항목이 없습니다
|
||||
|
|
@ -672,7 +675,7 @@ export function RepeaterTable({
|
|||
/>
|
||||
</td>
|
||||
{/* 데이터 컬럼들 */}
|
||||
{columns.map((col) => (
|
||||
{visibleColumns.map((col) => (
|
||||
<td
|
||||
key={col.field}
|
||||
className="overflow-hidden border-r border-b border-gray-200 px-1 py-1"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export interface RepeaterColumnConfig {
|
|||
calculated?: boolean; // 계산 필드 여부
|
||||
width?: string; // 컬럼 너비
|
||||
required?: boolean; // 필수 입력 여부
|
||||
hidden?: boolean; // 히든 필드 여부 (UI에서 숨기지만 데이터는 유지)
|
||||
defaultValue?: string | number | boolean; // 기본값
|
||||
selectOptions?: { value: string; label: string }[]; // select일 때 옵션
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
|
|||
calculated: col.calculated ?? false,
|
||||
width: col.width || "150px",
|
||||
required: col.required,
|
||||
hidden: col.hidden ?? false,
|
||||
defaultValue: col.defaultValue,
|
||||
selectOptions: col.selectOptions,
|
||||
// valueMapping은 별도로 처리
|
||||
|
|
|
|||
|
|
@ -82,8 +82,6 @@ const CalculationRuleEditor: React.FC<CalculationRuleEditorProps> = ({
|
|||
// 소스 테이블의 카테고리 컬럼 정보 로드
|
||||
useEffect(() => {
|
||||
const loadCategoryColumns = async () => {
|
||||
console.log("[CalculationRuleEditor] sourceTableName:", sourceTableName);
|
||||
|
||||
if (!sourceTableName) {
|
||||
setCategoryColumns({});
|
||||
return;
|
||||
|
|
@ -92,7 +90,6 @@ const CalculationRuleEditor: React.FC<CalculationRuleEditorProps> = ({
|
|||
try {
|
||||
const { getCategoryColumns } = await import("@/lib/api/tableCategoryValue");
|
||||
const result = await getCategoryColumns(sourceTableName);
|
||||
console.log("[CalculationRuleEditor] getCategoryColumns 결과:", result);
|
||||
|
||||
if (result && result.success && Array.isArray(result.data)) {
|
||||
const categoryMap: Record<string, boolean> = {};
|
||||
|
|
@ -103,7 +100,6 @@ const CalculationRuleEditor: React.FC<CalculationRuleEditorProps> = ({
|
|||
categoryMap[colName] = true;
|
||||
}
|
||||
});
|
||||
console.log("[CalculationRuleEditor] categoryMap:", categoryMap);
|
||||
setCategoryColumns(categoryMap);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -128,29 +124,18 @@ const CalculationRuleEditor: React.FC<CalculationRuleEditorProps> = ({
|
|||
const selectedColumn = columns.find((col) => col.field === conditionField);
|
||||
const actualFieldName = selectedColumn?.sourceField || conditionField;
|
||||
|
||||
console.log("[loadConditionOptions] 조건 필드:", {
|
||||
conditionField,
|
||||
actualFieldName,
|
||||
sourceTableName,
|
||||
categoryColumnsKeys: Object.keys(categoryColumns),
|
||||
isCategoryColumn: categoryColumns[actualFieldName],
|
||||
});
|
||||
|
||||
// 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인
|
||||
if (sourceTableName && categoryColumns[actualFieldName]) {
|
||||
try {
|
||||
setLoadingOptions(true);
|
||||
const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
|
||||
console.log("[loadConditionOptions] getCategoryValues 호출:", sourceTableName, actualFieldName);
|
||||
const result = await getCategoryValues(sourceTableName, actualFieldName, false);
|
||||
console.log("[loadConditionOptions] getCategoryValues 결과:", result);
|
||||
if (result && result.success && Array.isArray(result.data)) {
|
||||
const options = result.data.map((item: any) => ({
|
||||
// API 응답은 camelCase (valueCode, valueLabel)
|
||||
value: item.valueCode || item.value_code || item.value,
|
||||
label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value,
|
||||
}));
|
||||
console.log("[loadConditionOptions] 매핑된 옵션:", options);
|
||||
setCategoryOptions(options);
|
||||
} else {
|
||||
setCategoryOptions([]);
|
||||
|
|
@ -1094,6 +1079,14 @@ function ColumnSettingItem({
|
|||
/>
|
||||
<span>필수</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer" title="UI에서 숨기지만 데이터는 유지됩니다">
|
||||
<Switch
|
||||
checked={col.hidden ?? false}
|
||||
onCheckedChange={(checked) => onUpdate({ hidden: checked })}
|
||||
className="scale-75"
|
||||
/>
|
||||
<span>히든</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer" title="부모 화면에서 전달받은 값을 모든 행에 적용">
|
||||
<Switch
|
||||
checked={col.receiveFromParent ?? false}
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@ export interface TableColumnConfig {
|
|||
editable?: boolean; // 편집 가능 여부 (기본: true)
|
||||
calculated?: boolean; // 계산 필드 여부 (자동 읽기전용)
|
||||
required?: boolean; // 필수 입력 여부
|
||||
hidden?: boolean; // 히든 필드 여부 (UI에서 숨기지만 데이터는 유지)
|
||||
|
||||
// 너비 설정
|
||||
width?: string; // 기본 너비 (예: "150px")
|
||||
|
|
|
|||
Loading…
Reference in New Issue