feat(repeater): 하위 데이터 조회 및 조건부 입력 기능 구현, 테이블 선택 데이터 동기화 개선
Repeater 컴포넌트에 하위 데이터 조회 기능 추가 (재고/단가 조회) 조건부 입력 활성화 및 최대값 제한 기능 구현 필드 정의 순서 변경 기능 추가 (드래그앤드롭, 화살표 버튼) TableListComponent의 DataProvider 클로저 문제 해결 ButtonPrimaryComponent에 modalDataStore fallback 로직 추가
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
"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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user