조건부 계산식

This commit is contained in:
kjs 2025-12-31 14:17:39 +09:00
parent 417e1d297b
commit eb868965df
5 changed files with 33 additions and 34 deletions

View File

@ -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"

View File

@ -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일 때 옵션

View File

@ -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은 별도로 처리

View File

@ -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}

View File

@ -378,6 +378,7 @@ export interface TableColumnConfig {
editable?: boolean; // 편집 가능 여부 (기본: true)
calculated?: boolean; // 계산 필드 여부 (자동 읽기전용)
required?: boolean; // 필수 입력 여부
hidden?: boolean; // 히든 필드 여부 (UI에서 숨기지만 데이터는 유지)
// 너비 설정
width?: string; // 기본 너비 (예: "150px")