Repeater 컴포넌트에 하위 데이터 조회 기능 추가 (재고/단가 조회) 조건부 입력 활성화 및 최대값 제한 기능 구현 필드 정의 순서 변경 기능 추가 (드래그앤드롭, 화살표 버튼) TableListComponent의 DataProvider 클로저 문제 해결 ButtonPrimaryComponent에 modalDataStore fallback 로직 추가
228 lines
6.7 KiB
TypeScript
228 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { SubDataLookupConfig, SubDataState } from "@/types/repeater";
|
|
|
|
const LOG_PREFIX = {
|
|
INFO: "[SubDataLookup]",
|
|
DEBUG: "[SubDataLookup]",
|
|
WARN: "[SubDataLookup]",
|
|
ERROR: "[SubDataLookup]",
|
|
};
|
|
|
|
export interface UseSubDataLookupProps {
|
|
config: SubDataLookupConfig;
|
|
linkValue: string | number | null; // 상위 항목의 연결 값 (예: item_code)
|
|
itemIndex: number; // 상위 항목 인덱스
|
|
enabled?: boolean; // 기능 활성화 여부
|
|
}
|
|
|
|
export interface UseSubDataLookupReturn {
|
|
data: any[]; // 조회된 하위 데이터
|
|
isLoading: boolean; // 로딩 상태
|
|
error: string | null; // 에러 메시지
|
|
selectedItem: any | null; // 선택된 하위 항목
|
|
setSelectedItem: (item: any | null) => void; // 선택 항목 설정
|
|
isInputEnabled: boolean; // 조건부 입력 활성화 여부
|
|
maxValue: number | null; // 최대 입력 가능 값
|
|
isExpanded: boolean; // 확장 상태
|
|
setIsExpanded: (expanded: boolean) => void; // 확장 상태 설정
|
|
refetch: () => void; // 데이터 재조회
|
|
getSelectionSummary: () => string; // 선택 요약 텍스트
|
|
}
|
|
|
|
/**
|
|
* 하위 데이터 조회 훅
|
|
* 품목 선택 시 재고/단가 등 관련 데이터를 조회하고 관리
|
|
*/
|
|
export function useSubDataLookup(props: UseSubDataLookupProps): UseSubDataLookupReturn {
|
|
const { config, linkValue, itemIndex, enabled = true } = props;
|
|
|
|
// 상태
|
|
const [data, setData] = useState<any[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedItem, setSelectedItem] = useState<any | null>(null);
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
// 이전 linkValue 추적 (중복 호출 방지)
|
|
const prevLinkValueRef = useRef<string | number | null>(null);
|
|
|
|
// 데이터 조회 함수
|
|
const fetchData = useCallback(async () => {
|
|
// 비활성화 또는 linkValue 없으면 스킵
|
|
if (!enabled || !config?.enabled || !linkValue) {
|
|
console.log(`${LOG_PREFIX.DEBUG} 조회 스킵:`, {
|
|
enabled,
|
|
configEnabled: config?.enabled,
|
|
linkValue,
|
|
itemIndex,
|
|
});
|
|
setData([]);
|
|
setSelectedItem(null);
|
|
return;
|
|
}
|
|
|
|
const { tableName, linkColumn, additionalFilters } = config.lookup;
|
|
|
|
if (!tableName || !linkColumn) {
|
|
console.warn(`${LOG_PREFIX.WARN} 필수 설정 누락:`, { tableName, linkColumn });
|
|
return;
|
|
}
|
|
|
|
console.log(`${LOG_PREFIX.INFO} 하위 데이터 조회 시작:`, {
|
|
tableName,
|
|
linkColumn,
|
|
linkValue,
|
|
itemIndex,
|
|
});
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// 검색 조건 구성 - 정확한 값 매칭을 위해 equals 연산자 사용
|
|
const searchCondition: Record<string, any> = {
|
|
[linkColumn]: { value: linkValue, operator: "equals" },
|
|
...additionalFilters,
|
|
};
|
|
|
|
console.log(`${LOG_PREFIX.DEBUG} API 요청 조건:`, {
|
|
tableName,
|
|
linkColumn,
|
|
linkValue,
|
|
searchCondition,
|
|
});
|
|
|
|
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
|
page: 1,
|
|
size: 100,
|
|
search: searchCondition,
|
|
autoFilter: { enabled: true },
|
|
});
|
|
|
|
if (response.data?.success) {
|
|
const items = response.data?.data?.data || response.data?.data || [];
|
|
console.log(`${LOG_PREFIX.DEBUG} API 응답:`, {
|
|
dataCount: items.length,
|
|
firstItem: items[0],
|
|
tableName,
|
|
});
|
|
setData(items);
|
|
} else {
|
|
console.warn(`${LOG_PREFIX.WARN} API 응답 실패:`, response.data);
|
|
setData([]);
|
|
setError("데이터 조회에 실패했습니다");
|
|
}
|
|
} catch (err: any) {
|
|
console.error(`${LOG_PREFIX.ERROR} 하위 데이터 조회 실패:`, {
|
|
error: err.message,
|
|
config,
|
|
linkValue,
|
|
});
|
|
setError(err.message || "데이터 조회 중 오류가 발생했습니다");
|
|
setData([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [enabled, config, linkValue, itemIndex]);
|
|
|
|
// linkValue 변경 시 데이터 조회
|
|
useEffect(() => {
|
|
// 같은 값이면 스킵
|
|
if (prevLinkValueRef.current === linkValue) {
|
|
return;
|
|
}
|
|
prevLinkValueRef.current = linkValue;
|
|
|
|
// linkValue가 없으면 초기화
|
|
if (!linkValue) {
|
|
setData([]);
|
|
setSelectedItem(null);
|
|
setIsExpanded(false);
|
|
return;
|
|
}
|
|
|
|
fetchData();
|
|
}, [linkValue, fetchData]);
|
|
|
|
// 조건부 입력 활성화 여부 계산
|
|
const isInputEnabled = useCallback((): boolean => {
|
|
if (!config?.enabled || !selectedItem) {
|
|
return false;
|
|
}
|
|
|
|
const { requiredFields, requiredMode = "all" } = config.selection;
|
|
|
|
if (!requiredFields || requiredFields.length === 0) {
|
|
// 필수 필드가 없으면 선택만 하면 활성화
|
|
return true;
|
|
}
|
|
|
|
// 선택된 항목에서 필수 필드 값 확인
|
|
if (requiredMode === "any") {
|
|
// 하나라도 있으면 OK
|
|
return requiredFields.some((field) => {
|
|
const value = selectedItem[field];
|
|
return value !== undefined && value !== null && value !== "";
|
|
});
|
|
} else {
|
|
// 모두 있어야 OK
|
|
return requiredFields.every((field) => {
|
|
const value = selectedItem[field];
|
|
return value !== undefined && value !== null && value !== "";
|
|
});
|
|
}
|
|
}, [config, selectedItem]);
|
|
|
|
// 최대값 계산
|
|
const getMaxValue = useCallback((): number | null => {
|
|
if (!config?.enabled || !selectedItem) {
|
|
return null;
|
|
}
|
|
|
|
const { maxValueField } = config.conditionalInput;
|
|
if (!maxValueField) {
|
|
return null;
|
|
}
|
|
|
|
const maxValue = selectedItem[maxValueField];
|
|
return typeof maxValue === "number" ? maxValue : parseFloat(maxValue) || null;
|
|
}, [config, selectedItem]);
|
|
|
|
// 선택 요약 텍스트 생성
|
|
const getSelectionSummary = useCallback((): string => {
|
|
if (!selectedItem) {
|
|
return "선택 안됨";
|
|
}
|
|
|
|
const { displayColumns, columnLabels } = config.lookup;
|
|
const parts: string[] = [];
|
|
|
|
displayColumns.forEach((col) => {
|
|
const value = selectedItem[col];
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
const label = columnLabels?.[col] || col;
|
|
parts.push(`${label}: ${value}`);
|
|
}
|
|
});
|
|
|
|
return parts.length > 0 ? parts.join(", ") : "선택됨";
|
|
}, [selectedItem, config?.lookup]);
|
|
|
|
return {
|
|
data,
|
|
isLoading,
|
|
error,
|
|
selectedItem,
|
|
setSelectedItem,
|
|
isInputEnabled: isInputEnabled(),
|
|
maxValue: getMaxValue(),
|
|
isExpanded,
|
|
setIsExpanded,
|
|
refetch: fetchData,
|
|
getSelectionSummary,
|
|
};
|
|
}
|