버튼 수정

This commit is contained in:
dohyeons 2025-12-16 14:44:35 +09:00
parent eb56aec0a7
commit aacedc004c
2 changed files with 138 additions and 117 deletions

View File

@ -86,10 +86,10 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
// 접힌 상태 관리 (각 항목별) // 접힌 상태 관리 (각 항목별)
const [collapsedItems, setCollapsedItems] = useState<Set<number>>(new Set()); const [collapsedItems, setCollapsedItems] = useState<Set<number>>(new Set());
// 🆕 초기 계산 완료 여부 추적 (무한 루프 방지) // 🆕 초기 계산 완료 여부 추적 (무한 루프 방지)
const initialCalcDoneRef = useRef(false); const initialCalcDoneRef = useRef(false);
// 🆕 삭제된 항목 ID 목록 추적 (ref로 관리하여 즉시 반영) // 🆕 삭제된 항목 ID 목록 추적 (ref로 관리하여 즉시 반영)
const deletedItemIdsRef = useRef<string[]>([]); const deletedItemIdsRef = useRef<string[]>([]);
@ -121,45 +121,45 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
return; return;
} }
// 🆕 초기 로드 시 계산식 필드 자동 업데이트 (한 번만 실행) // 🆕 초기 로드 시 계산식 필드 자동 업데이트 (한 번만 실행)
const calculatedFields = fields.filter((f) => f.type === "calculated"); const calculatedFields = fields.filter((f) => f.type === "calculated");
if (calculatedFields.length > 0 && !initialCalcDoneRef.current) { if (calculatedFields.length > 0 && !initialCalcDoneRef.current) {
const updatedValue = value.map((item) => { const updatedValue = value.map((item) => {
const updatedItem = { ...item }; const updatedItem = { ...item };
let hasChange = false; let hasChange = false;
calculatedFields.forEach((calcField) => { calculatedFields.forEach((calcField) => {
const calculatedValue = calculateValue(calcField.formula, updatedItem); const calculatedValue = calculateValue(calcField.formula, updatedItem);
if (calculatedValue !== null && updatedItem[calcField.name] !== calculatedValue) { if (calculatedValue !== null && updatedItem[calcField.name] !== calculatedValue) {
updatedItem[calcField.name] = calculatedValue; updatedItem[calcField.name] = calculatedValue;
hasChange = true; hasChange = true;
}
});
// 🆕 기존 레코드임을 표시 (id가 있는 경우)
if (updatedItem.id) {
updatedItem._existingRecord = true;
} }
return hasChange ? updatedItem : item;
}); });
// 🆕 기존 레코드임을 표시 (id가 있는 경우) setItems(updatedValue);
if (updatedItem.id) { initialCalcDoneRef.current = true;
updatedItem._existingRecord = true;
} // 계산된 값이 있으면 onChange 호출 (초기 1회만)
const dataWithMeta = config.targetTable
return hasChange ? updatedItem : item; ? updatedValue.map((item) => ({ ...item, _targetTable: config.targetTable }))
}); : updatedValue;
onChange?.(dataWithMeta);
setItems(updatedValue); } else {
initialCalcDoneRef.current = true; // 🆕 기존 레코드 플래그 추가
// 계산된 값이 있으면 onChange 호출 (초기 1회만)
const dataWithMeta = config.targetTable
? updatedValue.map((item) => ({ ...item, _targetTable: config.targetTable }))
: updatedValue;
onChange?.(dataWithMeta);
} else {
// 🆕 기존 레코드 플래그 추가
const valueWithFlag = value.map((item) => ({ const valueWithFlag = value.map((item) => ({
...item, ...item,
_existingRecord: !!item.id, _existingRecord: !!item.id,
})); }));
setItems(valueWithFlag); setItems(valueWithFlag);
} }
}, [value]); }, [value]);
@ -184,14 +184,14 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
const handleRemoveItem = (index: number) => { const handleRemoveItem = (index: number) => {
// 🆕 항목이 1개 이하일 때도 삭제 가능 (빈 상태 허용) // 🆕 항목이 1개 이하일 때도 삭제 가능 (빈 상태 허용)
// minItems 체크 제거 - 모든 항목 삭제 허용 // minItems 체크 제거 - 모든 항목 삭제 허용
// 🆕 삭제되는 항목의 ID 저장 (DB에서 삭제할 때 필요) // 🆕 삭제되는 항목의 ID 저장 (DB에서 삭제할 때 필요)
const removedItem = items[index]; const removedItem = items[index];
if (removedItem?.id) { if (removedItem?.id) {
console.log("🗑️ [RepeaterInput] 삭제할 항목 ID 추가:", removedItem.id); console.log("🗑️ [RepeaterInput] 삭제할 항목 ID 추가:", removedItem.id);
deletedItemIdsRef.current = [...deletedItemIdsRef.current, removedItem.id]; deletedItemIdsRef.current = [...deletedItemIdsRef.current, removedItem.id];
} }
const newItems = items.filter((_, i) => i !== index); const newItems = items.filter((_, i) => i !== index);
setItems(newItems); setItems(newItems);
@ -199,15 +199,26 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
// 🆕 삭제된 항목 ID 목록도 함께 전달 (ref에서 최신값 사용) // 🆕 삭제된 항목 ID 목록도 함께 전달 (ref에서 최신값 사용)
const currentDeletedIds = deletedItemIdsRef.current; const currentDeletedIds = deletedItemIdsRef.current;
console.log("🗑️ [RepeaterInput] 현재 삭제 목록:", currentDeletedIds); console.log("🗑️ [RepeaterInput] 현재 삭제 목록:", currentDeletedIds);
const dataWithMeta = config.targetTable // 🆕 빈 배열일 때도 삭제 ID를 전달해야 함
? newItems.map((item, idx) => ({ let dataWithMeta: any[];
...item, if (config.targetTable) {
if (newItems.length > 0) {
// 항목이 있으면 첫 번째 항목에 삭제 ID 포함
dataWithMeta = newItems.map((item, idx) => ({
...item,
_targetTable: config.targetTable, _targetTable: config.targetTable,
// 첫 번째 항목에만 삭제 ID 목록 포함
...(idx === 0 ? { _deletedItemIds: currentDeletedIds } : {}), ...(idx === 0 ? { _deletedItemIds: currentDeletedIds } : {}),
})) }));
: newItems; } else if (currentDeletedIds.length > 0) {
// 🆕 모든 항목 삭제 시 삭제 ID만 포함된 메타 객체 전달
dataWithMeta = [{ _targetTable: config.targetTable, _deletedItemIds: currentDeletedIds, _deleteOnly: true }];
} else {
dataWithMeta = [];
}
} else {
dataWithMeta = newItems;
}
console.log("🗑️ [RepeaterInput] onChange 호출 - dataWithMeta:", dataWithMeta); console.log("🗑️ [RepeaterInput] onChange 호출 - dataWithMeta:", dataWithMeta);
onChange?.(dataWithMeta); onChange?.(dataWithMeta);
@ -225,7 +236,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
...newItems[itemIndex], ...newItems[itemIndex],
[fieldName]: value, [fieldName]: value,
}; };
// 🆕 계산식 필드 자동 업데이트: 변경된 항목의 모든 계산식 필드 값을 재계산 // 🆕 계산식 필드 자동 업데이트: 변경된 항목의 모든 계산식 필드 값을 재계산
const calculatedFields = fields.filter((f) => f.type === "calculated"); const calculatedFields = fields.filter((f) => f.type === "calculated");
calculatedFields.forEach((calcField) => { calculatedFields.forEach((calcField) => {
@ -234,7 +245,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
newItems[itemIndex][calcField.name] = calculatedValue; newItems[itemIndex][calcField.name] = calculatedValue;
} }
}); });
setItems(newItems); setItems(newItems);
console.log("✏️ RepeaterInput 필드 변경, onChange 호출:", { console.log("✏️ RepeaterInput 필드 변경, onChange 호출:", {
itemIndex, itemIndex,
@ -247,8 +258,8 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
// 🆕 삭제된 항목 ID 목록도 유지 // 🆕 삭제된 항목 ID 목록도 유지
const currentDeletedIds = deletedItemIdsRef.current; const currentDeletedIds = deletedItemIdsRef.current;
const dataWithMeta = config.targetTable const dataWithMeta = config.targetTable
? newItems.map((item, idx) => ({ ? newItems.map((item, idx) => ({
...item, ...item,
_targetTable: config.targetTable, _targetTable: config.targetTable,
// 첫 번째 항목에만 삭제 ID 목록 포함 (삭제된 항목이 있는 경우에만) // 첫 번째 항목에만 삭제 ID 목록 포함 (삭제된 항목이 있는 경우에만)
...(idx === 0 && currentDeletedIds.length > 0 ? { _deletedItemIds: currentDeletedIds } : {}), ...(idx === 0 && currentDeletedIds.length > 0 ? { _deletedItemIds: currentDeletedIds } : {}),
@ -308,12 +319,12 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
*/ */
const calculateValue = (formula: CalculationFormula | undefined, item: RepeaterItemData): number | null => { const calculateValue = (formula: CalculationFormula | undefined, item: RepeaterItemData): number | null => {
if (!formula || !formula.field1) return null; if (!formula || !formula.field1) return null;
const value1 = parseFloat(item[formula.field1]) || 0; const value1 = parseFloat(item[formula.field1]) || 0;
const value2 = formula.field2 ? parseFloat(item[formula.field2]) || 0 : (formula.constantValue ?? 0); const value2 = formula.field2 ? parseFloat(item[formula.field2]) || 0 : (formula.constantValue ?? 0);
let result: number; let result: number;
switch (formula.operator) { switch (formula.operator) {
case "+": case "+":
result = value1 + value2; result = value1 + value2;
@ -349,7 +360,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
default: default:
result = value1; result = value1;
} }
return result; return result;
}; };
@ -361,27 +372,27 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
*/ */
const formatNumber = (value: number | null, format?: RepeaterFieldDefinition["numberFormat"]): string => { const formatNumber = (value: number | null, format?: RepeaterFieldDefinition["numberFormat"]): string => {
if (value === null || isNaN(value)) return "-"; if (value === null || isNaN(value)) return "-";
let formattedValue = value; let formattedValue = value;
// 소수점 자릿수 적용 // 소수점 자릿수 적용
if (format?.decimalPlaces !== undefined) { if (format?.decimalPlaces !== undefined) {
formattedValue = parseFloat(value.toFixed(format.decimalPlaces)); formattedValue = parseFloat(value.toFixed(format.decimalPlaces));
} }
// 천 단위 구분자 // 천 단위 구분자
let result = let result =
format?.useThousandSeparator !== false format?.useThousandSeparator !== false
? formattedValue.toLocaleString("ko-KR", { ? formattedValue.toLocaleString("ko-KR", {
minimumFractionDigits: format?.minimumFractionDigits ?? 0, minimumFractionDigits: format?.minimumFractionDigits ?? 0,
maximumFractionDigits: format?.maximumFractionDigits ?? format?.decimalPlaces ?? 0, maximumFractionDigits: format?.maximumFractionDigits ?? format?.decimalPlaces ?? 0,
}) })
: formattedValue.toString(); : formattedValue.toString();
// 접두사/접미사 추가 // 접두사/접미사 추가
if (format?.prefix) result = format.prefix + result; if (format?.prefix) result = format.prefix + result;
if (format?.suffix) result = result + format.suffix; if (format?.suffix) result = result + format.suffix;
return result; return result;
}; };
@ -392,7 +403,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
// 🆕 placeholder 기본값: 필드에 설정된 값 > 필드 라벨 기반 자동 생성 // 🆕 placeholder 기본값: 필드에 설정된 값 > 필드 라벨 기반 자동 생성
// "id(를) 입력하세요" 같은 잘못된 기본값 방지 // "id(를) 입력하세요" 같은 잘못된 기본값 방지
const defaultPlaceholder = field.placeholder || `${field.label || field.name}`; const defaultPlaceholder = field.placeholder || `${field.label || field.name}`;
const commonProps = { const commonProps = {
value: value || "", value: value || "",
disabled: isReadonly, disabled: isReadonly,
@ -405,21 +416,21 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
const item = items[itemIndex]; const item = items[itemIndex];
const calculatedValue = calculateValue(field.formula, item); const calculatedValue = calculateValue(field.formula, item);
const formattedValue = formatNumber(calculatedValue, field.numberFormat); const formattedValue = formatNumber(calculatedValue, field.numberFormat);
return <span className="inline-block min-w-[80px] text-sm font-medium text-blue-700">{formattedValue}</span>; return <span className="inline-block min-w-[80px] text-sm font-medium text-blue-700">{formattedValue}</span>;
} }
// 카테고리 타입은 항상 배지로 표시 (카테고리 관리에서 설정한 색상 적용) // 카테고리 타입은 항상 배지로 표시 (카테고리 관리에서 설정한 색상 적용)
if (field.type === "category") { if (field.type === "category") {
if (!value) return <span className="text-muted-foreground text-sm">-</span>; if (!value) return <span className="text-muted-foreground text-sm">-</span>;
// field.name을 키로 사용 (테이블 리스트와 동일) // field.name을 키로 사용 (테이블 리스트와 동일)
const mapping = categoryMappings[field.name]; const mapping = categoryMappings[field.name];
const valueStr = String(value); // 값을 문자열로 변환 const valueStr = String(value); // 값을 문자열로 변환
const categoryData = mapping?.[valueStr]; const categoryData = mapping?.[valueStr];
const displayLabel = categoryData?.label || valueStr; const displayLabel = categoryData?.label || valueStr;
const displayColor = categoryData?.color || "#64748b"; // 기본 색상 (slate) const displayColor = categoryData?.color || "#64748b"; // 기본 색상 (slate)
console.log(`🏷️ [RepeaterInput] 카테고리 배지 렌더링:`, { console.log(`🏷️ [RepeaterInput] 카테고리 배지 렌더링:`, {
fieldName: field.name, fieldName: field.name,
value: valueStr, value: valueStr,
@ -428,12 +439,12 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
displayLabel, displayLabel,
displayColor, displayColor,
}); });
// 색상이 "none"이면 일반 텍스트로 표시 // 색상이 "none"이면 일반 텍스트로 표시
if (displayColor === "none") { if (displayColor === "none") {
return <span className="text-sm">{displayLabel}</span>; return <span className="text-sm">{displayLabel}</span>;
} }
return ( return (
<Badge <Badge
style={{ style={{
@ -455,7 +466,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
const option = field.options.find((opt) => opt.value === value); const option = field.options.find((opt) => opt.value === value);
return <span className="text-sm">{option?.label || value}</span>; return <span className="text-sm">{option?.label || value}</span>;
} }
// 🆕 카테고리 매핑이 있는 경우 라벨로 변환 (조인된 테이블의 카테고리 필드) // 🆕 카테고리 매핑이 있는 경우 라벨로 변환 (조인된 테이블의 카테고리 필드)
const mapping = categoryMappings[field.name]; const mapping = categoryMappings[field.name];
if (mapping && value) { if (mapping && value) {
@ -480,7 +491,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
return <span className="text-foreground text-sm">{categoryData.label}</span>; return <span className="text-foreground text-sm">{categoryData.label}</span>;
} }
} }
// 일반 텍스트 // 일반 텍스트
return <span className="text-foreground text-sm">{value || "-"}</span>; return <span className="text-foreground text-sm">{value || "-"}</span>;
} }
@ -555,12 +566,12 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
if (field.numberFormat?.useThousandSeparator || field.numberFormat?.prefix || field.numberFormat?.suffix) { if (field.numberFormat?.useThousandSeparator || field.numberFormat?.prefix || field.numberFormat?.suffix) {
const numValue = parseFloat(value) || 0; const numValue = parseFloat(value) || 0;
const formattedDisplay = formatNumber(numValue, field.numberFormat); const formattedDisplay = formatNumber(numValue, field.numberFormat);
// 읽기 전용이면 포맷팅된 텍스트만 표시 // 읽기 전용이면 포맷팅된 텍스트만 표시
if (isReadonly) { if (isReadonly) {
return <span className="inline-block min-w-[80px] text-sm">{formattedDisplay}</span>; return <span className="inline-block min-w-[80px] text-sm">{formattedDisplay}</span>;
} }
// 편집 가능: 입력은 숫자로, 표시는 포맷팅 // 편집 가능: 입력은 숫자로, 표시는 포맷팅
return ( return (
<div className="relative min-w-[80px]"> <div className="relative min-w-[80px]">
@ -576,7 +587,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
</div> </div>
); );
} }
return ( return (
<Input <Input
{...commonProps} {...commonProps}
@ -627,29 +638,29 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
// 카테고리 타입 필드 + readonly 필드 (조인된 테이블에서 온 데이터일 가능성) // 카테고리 타입 필드 + readonly 필드 (조인된 테이블에서 온 데이터일 가능성)
const categoryFields = fields.filter((f) => f.type === "category"); const categoryFields = fields.filter((f) => f.type === "category");
const readonlyFields = fields.filter((f) => f.displayMode === "readonly" && f.type === "text"); const readonlyFields = fields.filter((f) => f.displayMode === "readonly" && f.type === "text");
if (categoryFields.length === 0 && readonlyFields.length === 0) return; if (categoryFields.length === 0 && readonlyFields.length === 0) return;
const loadCategoryMappings = async () => { const loadCategoryMappings = async () => {
const apiClient = (await import("@/lib/api/client")).apiClient; const apiClient = (await import("@/lib/api/client")).apiClient;
// 1. 카테고리 타입 필드 매핑 로드 // 1. 카테고리 타입 필드 매핑 로드
for (const field of categoryFields) { for (const field of categoryFields) {
const columnName = field.name; const columnName = field.name;
if (categoryMappings[columnName]) continue; if (categoryMappings[columnName]) continue;
try { try {
const tableName = config.targetTable; const tableName = config.targetTable;
if (!tableName) continue; if (!tableName) continue;
console.log(`📡 [RepeaterInput] 카테고리 매핑 로드: ${tableName}/${columnName}`); console.log(`📡 [RepeaterInput] 카테고리 매핑 로드: ${tableName}/${columnName}`);
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color: string }> = {}; const mapping: Record<string, { label: string; color: string }> = {};
response.data.data.forEach((item: any) => { response.data.data.forEach((item: any) => {
const key = String(item.valueCode); const key = String(item.valueCode);
mapping[key] = { mapping[key] = {
@ -657,9 +668,9 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
color: item.color || "#64748b", color: item.color || "#64748b",
}; };
}); });
console.log(`✅ [RepeaterInput] 카테고리 매핑 로드 완료 [${columnName}]:`, mapping); console.log(`✅ [RepeaterInput] 카테고리 매핑 로드 완료 [${columnName}]:`, mapping);
setCategoryMappings((prev) => ({ setCategoryMappings((prev) => ({
...prev, ...prev,
[columnName]: mapping, [columnName]: mapping,
@ -669,29 +680,29 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
console.error(`❌ [RepeaterInput] 카테고리 매핑 로드 실패 (${columnName}):`, error); console.error(`❌ [RepeaterInput] 카테고리 매핑 로드 실패 (${columnName}):`, error);
} }
} }
// 2. 🆕 readonly 필드에 대해 조인된 테이블 (item_info)에서 카테고리 매핑 로드 // 2. 🆕 readonly 필드에 대해 조인된 테이블 (item_info)에서 카테고리 매핑 로드
// material, division 등 조인된 테이블의 카테고리 필드 // material, division 등 조인된 테이블의 카테고리 필드
const joinedTableFields = ["material", "division", "status", "currency_code"]; const joinedTableFields = ["material", "division", "status", "currency_code"];
const fieldsToLoadFromJoinedTable = readonlyFields.filter((f) => joinedTableFields.includes(f.name)); const fieldsToLoadFromJoinedTable = readonlyFields.filter((f) => joinedTableFields.includes(f.name));
if (fieldsToLoadFromJoinedTable.length > 0) { if (fieldsToLoadFromJoinedTable.length > 0) {
// item_info 테이블에서 카테고리 매핑 로드 // item_info 테이블에서 카테고리 매핑 로드
const joinedTableName = "item_info"; const joinedTableName = "item_info";
for (const field of fieldsToLoadFromJoinedTable) { for (const field of fieldsToLoadFromJoinedTable) {
const columnName = field.name; const columnName = field.name;
if (categoryMappings[columnName]) continue; if (categoryMappings[columnName]) continue;
try { try {
console.log(`📡 [RepeaterInput] 조인 테이블 카테고리 매핑 로드: ${joinedTableName}/${columnName}`); console.log(`📡 [RepeaterInput] 조인 테이블 카테고리 매핑 로드: ${joinedTableName}/${columnName}`);
const response = await apiClient.get(`/table-categories/${joinedTableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${joinedTableName}/${columnName}/values`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color: string }> = {}; const mapping: Record<string, { label: string; color: string }> = {};
response.data.data.forEach((item: any) => { response.data.data.forEach((item: any) => {
const key = String(item.valueCode); const key = String(item.valueCode);
mapping[key] = { mapping[key] = {
@ -699,9 +710,9 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
color: item.color || "#64748b", color: item.color || "#64748b",
}; };
}); });
console.log(`✅ [RepeaterInput] 조인 테이블 카테고리 매핑 로드 완료 [${columnName}]:`, mapping); console.log(`✅ [RepeaterInput] 조인 테이블 카테고리 매핑 로드 완료 [${columnName}]:`, mapping);
setCategoryMappings((prev) => ({ setCategoryMappings((prev) => ({
...prev, ...prev,
[columnName]: mapping, [columnName]: mapping,

View File

@ -802,6 +802,7 @@ export class ButtonActionExecutor {
} }
} }
// 🆕 배열이 비어있지 않거나, 하나라도 항목이 있으면 처리
if (Array.isArray(parsedValue) && parsedValue.length > 0) { if (Array.isArray(parsedValue) && parsedValue.length > 0) {
const firstItem = parsedValue[0]; const firstItem = parsedValue[0];
const deletedItemIds = firstItem?._deletedItemIds; const deletedItemIds = firstItem?._deletedItemIds;
@ -811,6 +812,7 @@ export class ButtonActionExecutor {
firstItemKeys: firstItem ? Object.keys(firstItem) : [], firstItemKeys: firstItem ? Object.keys(firstItem) : [],
deletedItemIds, deletedItemIds,
targetTable, targetTable,
isDeleteOnly: firstItem?._deleteOnly,
}); });
if (deletedItemIds && deletedItemIds.length > 0 && targetTable) { if (deletedItemIds && deletedItemIds.length > 0 && targetTable) {
@ -868,6 +870,12 @@ export class ButtonActionExecutor {
// _targetTable이 없거나, _repeatScreenModal_ 키면 스킵 (다른 로직에서 처리) // _targetTable이 없거나, _repeatScreenModal_ 키면 스킵 (다른 로직에서 처리)
if (!repeaterTargetTable || fieldKey.startsWith("_repeatScreenModal_")) continue; if (!repeaterTargetTable || fieldKey.startsWith("_repeatScreenModal_")) continue;
// 🆕 _deleteOnly 플래그가 있으면 INSERT/UPDATE 건너뛰기 (삭제 전용)
if (firstItem?._deleteOnly) {
console.log(`⏭️ [handleSave] 삭제 전용 데이터 - INSERT/UPDATE 스킵: ${fieldKey}`);
continue;
}
console.log(`📦 [handleSave] RepeaterFieldGroup 데이터 저장: ${fieldKey}${repeaterTargetTable}`, { console.log(`📦 [handleSave] RepeaterFieldGroup 데이터 저장: ${fieldKey}${repeaterTargetTable}`, {
itemCount: parsedData.length, itemCount: parsedData.length,
}); });
@ -2839,10 +2847,7 @@ export class ButtonActionExecutor {
* EditModal public으로 * EditModal public으로
* *
*/ */
public static async executeAfterSaveControl( public static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<void> {
config: ButtonActionConfig,
context: ButtonActionContext,
): Promise<void> {
console.log("🎯 저장 후 제어 실행:", { console.log("🎯 저장 후 제어 실행:", {
enableDataflowControl: config.enableDataflowControl, enableDataflowControl: config.enableDataflowControl,
dataflowConfig: config.dataflowConfig, dataflowConfig: config.dataflowConfig,
@ -4285,7 +4290,7 @@ export class ButtonActionExecutor {
// 추적 중인지 확인 (새로고침 후에도 DB 상태 기반 종료 가능하도록 수정) // 추적 중인지 확인 (새로고침 후에도 DB 상태 기반 종료 가능하도록 수정)
const isTrackingActive = !!this.trackingIntervalId; const isTrackingActive = !!this.trackingIntervalId;
if (!isTrackingActive) { if (!isTrackingActive) {
// 추적 중이 아니어도 DB 상태 변경은 진행 (새로고침 후 종료 지원) // 추적 중이 아니어도 DB 상태 변경은 진행 (새로고침 후 종료 지원)
console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행"); console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행");
@ -4301,25 +4306,26 @@ export class ButtonActionExecutor {
let dbDeparture: string | null = null; let dbDeparture: string | null = null;
let dbArrival: string | null = null; let dbArrival: string | null = null;
let dbVehicleId: string | null = null; let dbVehicleId: string | null = null;
const userId = context.userId || this.trackingUserId; const userId = context.userId || this.trackingUserId;
if (userId) { if (userId) {
try { try {
const { apiClient } = await import("@/lib/api/client"); const { apiClient } = await import("@/lib/api/client");
const statusTableName = config.trackingStatusTableName || this.trackingConfig?.trackingStatusTableName || context.tableName || "vehicles"; const statusTableName =
config.trackingStatusTableName ||
this.trackingConfig?.trackingStatusTableName ||
context.tableName ||
"vehicles";
const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id"; const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id";
// DB에서 현재 차량 정보 조회 // DB에서 현재 차량 정보 조회
const vehicleResponse = await apiClient.post( const vehicleResponse = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
`/table-management/tables/${statusTableName}/data`, page: 1,
{ size: 1,
page: 1, search: { [keyField]: userId },
size: 1, autoFilter: true,
search: { [keyField]: userId }, });
autoFilter: true,
},
);
const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0]; const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0];
if (vehicleData) { if (vehicleData) {
dbDeparture = vehicleData.departure || null; dbDeparture = vehicleData.departure || null;
@ -4335,14 +4341,18 @@ export class ButtonActionExecutor {
// 마지막 위치 저장 (추적 중이었던 경우에만) // 마지막 위치 저장 (추적 중이었던 경우에만)
if (isTrackingActive) { if (isTrackingActive) {
// DB 값 우선, 없으면 formData 사용 // DB 값 우선, 없으면 formData 사용
const departure = dbDeparture || const departure =
this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null; dbDeparture ||
const arrival = dbArrival || this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] ||
this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null; null;
const arrival =
dbArrival || this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null;
const departureName = this.trackingContext?.formData?.["departure_name"] || null; const departureName = this.trackingContext?.formData?.["departure_name"] || null;
const destinationName = this.trackingContext?.formData?.["destination_name"] || null; const destinationName = this.trackingContext?.formData?.["destination_name"] || null;
const vehicleId = dbVehicleId || const vehicleId =
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null; dbVehicleId ||
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] ||
null;
await this.saveLocationToHistory( await this.saveLocationToHistory(
tripId, tripId,