리피터 수정
This commit is contained in:
parent
c3adb4216f
commit
5c098a0395
|
|
@ -302,6 +302,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
|
||||
allComponents={allComponents} // 🆕 연쇄 드롭다운 부모 감지용
|
||||
currentComponent={selectedComponent} // 🆕 현재 컴포넌트 정보
|
||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 (채번 규칙 등)
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* RepeaterTable 및 ItemSelectionModal 재사용
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
DEFAULT_REPEATER_CONFIG,
|
||||
} from "@/types/unified-repeater";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
|
||||
// modal-repeater-table 컴포넌트 재사용
|
||||
import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/RepeaterTable";
|
||||
|
|
@ -334,6 +335,8 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
width: col.width === "auto" ? undefined : col.width,
|
||||
required: false,
|
||||
categoryRef, // 🆕 카테고리 참조 ID 전달
|
||||
hidden: col.hidden, // 🆕 히든 처리
|
||||
autoFill: col.autoFill, // 🆕 자동 입력 설정
|
||||
};
|
||||
});
|
||||
}, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]);
|
||||
|
|
@ -436,53 +439,133 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
}, [data, selectedRows, handleDataChange]);
|
||||
|
||||
// 행 추가 (inline 모드)
|
||||
const handleAddRow = useCallback(() => {
|
||||
// 🆕 자동 입력 값 생성 함수 (동기 - 채번 제외)
|
||||
const generateAutoFillValueSync = useCallback(
|
||||
(col: any, rowIndex: number, mainFormData?: Record<string, unknown>) => {
|
||||
if (!col.autoFill || col.autoFill.type === "none") return undefined;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
switch (col.autoFill.type) {
|
||||
case "currentDate":
|
||||
return now.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
|
||||
case "currentDateTime":
|
||||
return now.toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:mm:ss
|
||||
|
||||
case "sequence":
|
||||
return rowIndex + 1; // 1부터 시작하는 순번
|
||||
|
||||
case "numbering":
|
||||
// 채번은 별도 비동기 처리 필요
|
||||
return null; // null 반환하여 비동기 처리 필요함을 표시
|
||||
|
||||
case "fromMainForm":
|
||||
if (col.autoFill.sourceField && mainFormData) {
|
||||
return mainFormData[col.autoFill.sourceField];
|
||||
}
|
||||
return "";
|
||||
|
||||
case "fixed":
|
||||
return col.autoFill.fixedValue ?? "";
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 🆕 채번 API 호출 (비동기)
|
||||
const generateNumberingCode = useCallback(async (ruleId: string): Promise<string> => {
|
||||
try {
|
||||
const result = await allocateNumberingCode(ruleId);
|
||||
if (result.success && result.data?.generatedCode) {
|
||||
return result.data.generatedCode;
|
||||
}
|
||||
console.error("채번 실패:", result.error);
|
||||
return "";
|
||||
} catch (error) {
|
||||
console.error("채번 API 호출 실패:", error);
|
||||
return "";
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 🆕 행 추가 (inline 모드 또는 모달 열기) - 비동기로 변경
|
||||
const handleAddRow = useCallback(async () => {
|
||||
if (isModalMode) {
|
||||
setModalOpen(true);
|
||||
} else {
|
||||
const newRow: any = { _id: `new_${Date.now()}` };
|
||||
config.columns.forEach((col) => {
|
||||
newRow[col.key] = "";
|
||||
});
|
||||
const newData = [...data, newRow];
|
||||
handleDataChange(newData); // 🆕 handleDataChange 사용
|
||||
}
|
||||
}, [isModalMode, config.columns, data, handleDataChange]);
|
||||
|
||||
// 모달에서 항목 선택 - 🆕 columns 배열에서 isSourceDisplay 플래그로 구분
|
||||
const handleSelectItems = useCallback(
|
||||
(items: Record<string, unknown>[]) => {
|
||||
const fkColumn = config.dataSource?.foreignKey;
|
||||
|
||||
const newRows = items.map((item) => {
|
||||
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
|
||||
|
||||
// FK 값 저장 (resolvedReferenceKey 사용)
|
||||
if (fkColumn && item[resolvedReferenceKey]) {
|
||||
row[fkColumn] = item[resolvedReferenceKey];
|
||||
const currentRowCount = data.length;
|
||||
|
||||
// 먼저 동기적 자동 입력 값 적용
|
||||
for (const col of config.columns) {
|
||||
const autoValue = generateAutoFillValueSync(col, currentRowCount);
|
||||
if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) {
|
||||
// 채번 규칙: 즉시 API 호출
|
||||
newRow[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId);
|
||||
} else if (autoValue !== undefined) {
|
||||
newRow[col.key] = autoValue;
|
||||
} else {
|
||||
newRow[col.key] = "";
|
||||
}
|
||||
}
|
||||
|
||||
const newData = [...data, newRow];
|
||||
handleDataChange(newData);
|
||||
}
|
||||
}, [isModalMode, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode]);
|
||||
|
||||
// 모든 컬럼 처리 (순서대로)
|
||||
config.columns.forEach((col) => {
|
||||
if (col.isSourceDisplay) {
|
||||
// 소스 표시 컬럼: 소스 테이블에서 값 복사 (읽기 전용)
|
||||
row[`_display_${col.key}`] = item[col.key] || "";
|
||||
} else {
|
||||
// 입력 컬럼: 빈 값으로 초기화
|
||||
if (row[col.key] === undefined) {
|
||||
row[col.key] = "";
|
||||
// 모달에서 항목 선택 - 비동기로 변경
|
||||
const handleSelectItems = useCallback(
|
||||
async (items: Record<string, unknown>[]) => {
|
||||
const fkColumn = config.dataSource?.foreignKey;
|
||||
const currentRowCount = data.length;
|
||||
|
||||
// 채번이 필요한 컬럼 찾기
|
||||
const numberingColumns = config.columns.filter(
|
||||
(col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId
|
||||
);
|
||||
|
||||
const newRows = await Promise.all(
|
||||
items.map(async (item, index) => {
|
||||
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
|
||||
|
||||
// FK 값 저장 (resolvedReferenceKey 사용)
|
||||
if (fkColumn && item[resolvedReferenceKey]) {
|
||||
row[fkColumn] = item[resolvedReferenceKey];
|
||||
}
|
||||
|
||||
// 모든 컬럼 처리 (순서대로)
|
||||
for (const col of config.columns) {
|
||||
if (col.isSourceDisplay) {
|
||||
// 소스 표시 컬럼: 소스 테이블에서 값 복사 (읽기 전용)
|
||||
row[`_display_${col.key}`] = item[col.key] || "";
|
||||
} else {
|
||||
// 자동 입력 값 적용
|
||||
const autoValue = generateAutoFillValueSync(col, currentRowCount + index);
|
||||
if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) {
|
||||
// 채번 규칙: 즉시 API 호출
|
||||
row[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId);
|
||||
} else if (autoValue !== undefined) {
|
||||
row[col.key] = autoValue;
|
||||
} else if (row[col.key] === undefined) {
|
||||
// 입력 컬럼: 빈 값으로 초기화
|
||||
row[col.key] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
return row;
|
||||
})
|
||||
);
|
||||
|
||||
const newData = [...data, ...newRows];
|
||||
handleDataChange(newData); // 🆕 handleDataChange 사용하여 autoWidthTrigger도 증가
|
||||
handleDataChange(newData);
|
||||
setModalOpen(false);
|
||||
},
|
||||
[config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange],
|
||||
[config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode],
|
||||
);
|
||||
|
||||
// 소스 컬럼 목록 (모달용) - 🆕 columns 배열에서 isSourceDisplay인 것만 필터링
|
||||
|
|
@ -493,6 +576,61 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
.filter((key) => key && key !== "none");
|
||||
}, [config.columns]);
|
||||
|
||||
// 🆕 beforeFormSave 이벤트에서 채번 placeholder를 실제 값으로 변환
|
||||
const dataRef = useRef(data);
|
||||
dataRef.current = data;
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeFormSave = async (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
const formData = customEvent.detail?.formData;
|
||||
|
||||
if (!formData || !dataRef.current.length) return;
|
||||
|
||||
// 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환
|
||||
const processedData = await Promise.all(
|
||||
dataRef.current.map(async (row) => {
|
||||
const newRow = { ...row };
|
||||
|
||||
for (const key of Object.keys(newRow)) {
|
||||
const value = newRow[key];
|
||||
if (typeof value === "string" && value.startsWith("__NUMBERING_RULE__")) {
|
||||
// __NUMBERING_RULE__ruleId__ 형식에서 ruleId 추출
|
||||
const match = value.match(/__NUMBERING_RULE__(.+)__/);
|
||||
if (match) {
|
||||
const ruleId = match[1];
|
||||
try {
|
||||
const result = await allocateNumberingCode(ruleId);
|
||||
if (result.success && result.data?.generatedCode) {
|
||||
newRow[key] = result.data.generatedCode;
|
||||
} else {
|
||||
console.error("채번 실패:", result.error);
|
||||
newRow[key] = ""; // 채번 실패 시 빈 값
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("채번 API 호출 실패:", error);
|
||||
newRow[key] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
);
|
||||
|
||||
// 처리된 데이터를 formData에 추가
|
||||
const fieldName = config.fieldName || "repeaterData";
|
||||
formData[fieldName] = processedData;
|
||||
};
|
||||
|
||||
window.addEventListener("beforeFormSave", handleBeforeFormSave);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeFormSave", handleBeforeFormSave);
|
||||
};
|
||||
}, [config.fieldName]);
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* 헤더 영역 */}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,15 @@ import {
|
|||
GripVertical,
|
||||
ArrowRight,
|
||||
Calculator,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
|
||||
import { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
UnifiedRepeaterConfig,
|
||||
|
|
@ -41,6 +48,7 @@ interface UnifiedRepeaterConfigPanelProps {
|
|||
currentTableName?: string;
|
||||
screenTableName?: string;
|
||||
tableColumns?: any[];
|
||||
menuObjid?: number | string; // 🆕 메뉴 ID (채번 규칙 조회용)
|
||||
}
|
||||
|
||||
interface ColumnOption {
|
||||
|
|
@ -76,6 +84,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
onChange,
|
||||
currentTableName: propCurrentTableName,
|
||||
screenTableName,
|
||||
menuObjid,
|
||||
}) => {
|
||||
const currentTableName = screenTableName || propCurrentTableName;
|
||||
|
||||
|
|
@ -107,6 +116,88 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
|
||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
|
||||
|
||||
// 🆕 확장된 컬럼 (상세 설정 표시용)
|
||||
const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
|
||||
|
||||
// 🆕 채번 규칙 목록
|
||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||
const [loadingNumberingRules, setLoadingNumberingRules] = useState(false);
|
||||
|
||||
// 🆕 대상 메뉴 목록 (채번 규칙 선택용)
|
||||
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
||||
const [loadingMenus, setLoadingMenus] = useState(false);
|
||||
|
||||
// 🆕 선택된 메뉴 OBJID (컬럼별로 저장, 한 번 선택하면 공유)
|
||||
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
|
||||
// 기존 config에서 저장된 값이 있으면 복원
|
||||
const existingAutoFill = config.columns.find(c => c.autoFill?.type === "numbering" && c.autoFill.selectedMenuObjid);
|
||||
return existingAutoFill?.autoFill?.selectedMenuObjid || (menuObjid ? Number(menuObjid) : undefined);
|
||||
});
|
||||
|
||||
// 자동 입력 타입 옵션
|
||||
const autoFillOptions = [
|
||||
{ value: "none", label: "없음" },
|
||||
{ value: "currentDate", label: "현재 날짜" },
|
||||
{ value: "currentDateTime", label: "현재 날짜+시간" },
|
||||
{ value: "sequence", label: "순번 (1, 2, 3...)" },
|
||||
{ value: "numbering", label: "채번 규칙" },
|
||||
{ value: "fromMainForm", label: "메인 폼에서 복사" },
|
||||
{ value: "fixed", label: "고정값" },
|
||||
];
|
||||
|
||||
// 🆕 대상 메뉴 목록 로드 (사용자 메뉴의 레벨 2)
|
||||
useEffect(() => {
|
||||
const loadMenus = async () => {
|
||||
setLoadingMenus(true);
|
||||
try {
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const response = await apiClient.get("/admin/menus");
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const allMenus = response.data.data;
|
||||
|
||||
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
|
||||
const level2UserMenus = allMenus.filter((menu: any) =>
|
||||
menu.menu_type === '1' && menu.lev === 2
|
||||
);
|
||||
|
||||
setParentMenus(level2UserMenus);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("부모 메뉴 로드 실패:", error);
|
||||
} finally {
|
||||
setLoadingMenus(false);
|
||||
}
|
||||
};
|
||||
loadMenus();
|
||||
}, []);
|
||||
|
||||
// 🆕 채번 규칙 로드 (선택된 메뉴 기준)
|
||||
useEffect(() => {
|
||||
const loadNumberingRules = async () => {
|
||||
// 메뉴가 선택되지 않았으면 로드하지 않음
|
||||
if (!selectedMenuObjid) {
|
||||
setNumberingRules([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingNumberingRules(true);
|
||||
try {
|
||||
const result = await getAvailableNumberingRules(selectedMenuObjid);
|
||||
|
||||
if (result?.success && result.data) {
|
||||
setNumberingRules(result.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("채번 규칙 로드 실패:", error);
|
||||
setNumberingRules([]);
|
||||
} finally {
|
||||
setLoadingNumberingRules(false);
|
||||
}
|
||||
};
|
||||
loadNumberingRules();
|
||||
}, [selectedMenuObjid]);
|
||||
|
||||
// 설정 업데이트 헬퍼
|
||||
const updateConfig = useCallback(
|
||||
|
|
@ -639,62 +730,262 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
</Label>
|
||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||
{config.columns.map((col, index) => (
|
||||
<div
|
||||
key={col.key}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border p-2",
|
||||
col.isSourceDisplay ? "bg-blue-50 border-blue-200" : "bg-muted/30",
|
||||
)}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData("columnIndex", String(index));
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData("columnIndex"), 10);
|
||||
if (fromIndex !== index) {
|
||||
const newColumns = [...config.columns];
|
||||
const [movedCol] = newColumns.splice(fromIndex, 1);
|
||||
newColumns.splice(index, 0, movedCol);
|
||||
updateConfig({ columns: newColumns });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
|
||||
{col.isSourceDisplay ? (
|
||||
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
||||
) : (
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
<Input
|
||||
value={col.title}
|
||||
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
|
||||
placeholder="제목"
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
{!col.isSourceDisplay && (
|
||||
<Checkbox
|
||||
checked={col.editable ?? true}
|
||||
onCheckedChange={(checked) => updateColumnProp(col.key, "editable", !!checked)}
|
||||
title="편집 가능"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (col.isSourceDisplay) {
|
||||
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
||||
} else {
|
||||
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
||||
<div key={col.key} className="space-y-1">
|
||||
{/* 컬럼 헤더 (드래그 가능) */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border p-2",
|
||||
col.isSourceDisplay ? "border-blue-200 bg-blue-50/50" : "border-gray-200 bg-muted/30",
|
||||
col.hidden && "opacity-50",
|
||||
)}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData("columnIndex", String(index));
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData("columnIndex"), 10);
|
||||
if (fromIndex !== index) {
|
||||
const newColumns = [...config.columns];
|
||||
const [movedCol] = newColumns.splice(fromIndex, 1);
|
||||
newColumns.splice(index, 0, movedCol);
|
||||
updateConfig({ columns: newColumns });
|
||||
}
|
||||
}}
|
||||
className="text-destructive h-6 w-6 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
|
||||
|
||||
{/* 확장/축소 버튼 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedColumn(expandedColumn === col.key ? null : col.key)}
|
||||
className="p-0.5 hover:bg-gray-200 rounded"
|
||||
>
|
||||
{expandedColumn === col.key ? (
|
||||
<ChevronDown className="h-3 w-3 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{col.isSourceDisplay ? (
|
||||
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
||||
) : (
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<Input
|
||||
value={col.title}
|
||||
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
|
||||
placeholder="제목"
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
|
||||
{/* 히든 토글 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
|
||||
className={cn(
|
||||
"p-1 rounded hover:bg-gray-200",
|
||||
col.hidden ? "text-gray-400" : "text-gray-600",
|
||||
)}
|
||||
title={col.hidden ? "히든 (저장만 됨)" : "표시됨"}
|
||||
>
|
||||
{col.hidden ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 자동입력 표시 아이콘 */}
|
||||
{!col.isSourceDisplay && col.autoFill?.type && col.autoFill.type !== "none" && (
|
||||
<Wand2 className="h-3 w-3 text-purple-500 flex-shrink-0" title="자동 입력" />
|
||||
)}
|
||||
|
||||
{/* 편집 가능 체크박스 */}
|
||||
{!col.isSourceDisplay && (
|
||||
<Checkbox
|
||||
checked={col.editable ?? true}
|
||||
onCheckedChange={(checked) => updateColumnProp(col.key, "editable", !!checked)}
|
||||
title="편집 가능"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (col.isSourceDisplay) {
|
||||
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
||||
} else {
|
||||
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
||||
}
|
||||
}}
|
||||
className="text-destructive h-6 w-6 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 확장된 상세 설정 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && expandedColumn === col.key && (
|
||||
<div className="ml-6 space-y-2 rounded-md border border-dashed border-gray-300 bg-gray-50 p-2">
|
||||
{/* 자동 입력 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-gray-600">자동 입력</Label>
|
||||
<Select
|
||||
value={col.autoFill?.type || "none"}
|
||||
onValueChange={(value) => {
|
||||
const autoFill = value === "none" ? undefined : { type: value as any };
|
||||
updateColumnProp(col.key, "autoFill", autoFill);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{autoFillOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 채번 규칙 선택 */}
|
||||
{col.autoFill?.type === "numbering" && (
|
||||
<div className="space-y-2">
|
||||
{/* 대상 메뉴 선택 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-gray-600">
|
||||
대상 메뉴 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedMenuObjid?.toString() || ""}
|
||||
onValueChange={(value) => {
|
||||
const menuObjidNum = parseInt(value);
|
||||
setSelectedMenuObjid(menuObjidNum);
|
||||
|
||||
// 컬럼 설정에도 저장하여 유지
|
||||
updateColumnProp(col.key, "autoFill", {
|
||||
...col.autoFill,
|
||||
selectedMenuObjid: menuObjidNum,
|
||||
numberingRuleId: undefined, // 메뉴 변경 시 규칙 초기화
|
||||
});
|
||||
}}
|
||||
disabled={loadingMenus}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{parentMenus.length === 0 ? (
|
||||
<SelectItem value="no-menus" disabled>
|
||||
사용 가능한 메뉴가 없습니다
|
||||
</SelectItem>
|
||||
) : (
|
||||
parentMenus.map((menu) => (
|
||||
<SelectItem key={menu.objid} value={menu.objid.toString()}>
|
||||
{menu.menu_name_kor}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
이 입력 필드가 어느 메뉴에 속할지 선택하세요 (해당 메뉴의 채번규칙이 적용됩니다)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 채번 규칙 선택 (메뉴 선택 후) */}
|
||||
{selectedMenuObjid ? (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-gray-600">
|
||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
{loadingNumberingRules ? (
|
||||
<p className="text-[10px] text-gray-400">규칙 로딩 중...</p>
|
||||
) : numberingRules.length === 0 ? (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-[10px] text-amber-800">
|
||||
선택된 메뉴에 사용 가능한 채번 규칙이 없습니다
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={col.autoFill?.numberingRuleId || ""}
|
||||
onValueChange={(value) => updateColumnProp(col.key, "autoFill", {
|
||||
...col.autoFill,
|
||||
selectedMenuObjid,
|
||||
numberingRuleId: value,
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="채번 규칙 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{numberingRules.map((rule) => (
|
||||
<SelectItem key={rule.ruleId} value={rule.ruleId} className="text-xs">
|
||||
{rule.ruleName}
|
||||
{rule.description && (
|
||||
<span className="text-muted-foreground ml-2">
|
||||
- {rule.description}
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{col.autoFill?.numberingRuleId && (
|
||||
<p className="text-[10px] text-green-600">
|
||||
저장 시 채번 API를 통해 자동 생성됩니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-[10px] text-amber-800">
|
||||
먼저 대상 메뉴를 선택하세요
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 메인 폼에서 복사 설정 */}
|
||||
{col.autoFill?.type === "fromMainForm" && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-gray-600">복사할 필드명</Label>
|
||||
<Input
|
||||
value={col.autoFill?.sourceField || ""}
|
||||
onChange={(e) => updateColumnProp(col.key, "autoFill", {
|
||||
...col.autoFill,
|
||||
sourceField: e.target.value,
|
||||
})}
|
||||
placeholder="order_no"
|
||||
className="h-6 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 고정값 설정 */}
|
||||
{col.autoFill?.type === "fixed" && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-gray-600">고정값</Label>
|
||||
<Input
|
||||
value={String(col.autoFill?.fixedValue || "")}
|
||||
onChange={(e) => updateColumnProp(col.key, "autoFill", {
|
||||
...col.autoFill,
|
||||
fixedValue: e.target.value,
|
||||
})}
|
||||
placeholder="고정값 입력"
|
||||
className="h-6 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,28 @@ export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
|
|||
// 컬럼 너비 옵션
|
||||
export type ColumnWidthOption = "auto" | "60px" | "80px" | "100px" | "120px" | "150px" | "200px" | "250px" | "300px";
|
||||
|
||||
// 자동 입력 타입
|
||||
export type AutoFillType =
|
||||
| "none" // 자동 입력 없음
|
||||
| "currentDate" // 현재 날짜
|
||||
| "currentDateTime"// 현재 날짜+시간
|
||||
| "sequence" // 순번 (1, 2, 3...)
|
||||
| "numbering" // 채번 규칙 (관리자가 등록한 규칙 선택)
|
||||
| "fromMainForm" // 메인 폼에서 값 복사
|
||||
| "fixed"; // 고정값
|
||||
|
||||
// 자동 입력 설정
|
||||
export interface AutoFillConfig {
|
||||
type: AutoFillType;
|
||||
// fromMainForm 타입용
|
||||
sourceField?: string; // 메인 폼에서 복사할 필드명
|
||||
// fixed 타입용
|
||||
fixedValue?: string | number | boolean;
|
||||
// numbering 타입용 - 기존 채번 규칙 ID를 참조
|
||||
numberingRuleId?: string; // 채번 규칙 ID (numbering_rules 테이블)
|
||||
selectedMenuObjid?: number; // 🆕 채번 규칙 선택을 위한 대상 메뉴 OBJID
|
||||
}
|
||||
|
||||
// 컬럼 설정
|
||||
export interface RepeaterColumnConfig {
|
||||
key: string;
|
||||
|
|
@ -23,12 +45,15 @@ export interface RepeaterColumnConfig {
|
|||
width: ColumnWidthOption;
|
||||
visible: boolean;
|
||||
editable?: boolean; // 편집 가능 여부 (inline 모드)
|
||||
hidden?: boolean; // 🆕 히든 처리 (화면에 안 보이지만 저장됨)
|
||||
isJoinColumn?: boolean;
|
||||
sourceTable?: string;
|
||||
// 🆕 소스 테이블 표시 컬럼 여부 (modal 모드에서 읽기 전용으로 표시)
|
||||
isSourceDisplay?: boolean;
|
||||
// 입력 타입 (테이블 타입 관리의 inputType을 따름)
|
||||
inputType?: string; // text, number, date, code, entity 등
|
||||
// 🆕 자동 입력 설정
|
||||
autoFill?: AutoFillConfig;
|
||||
// 입력 타입별 상세 설정
|
||||
detailSettings?: {
|
||||
codeGroup?: string; // code 타입용
|
||||
|
|
|
|||
Loading…
Reference in New Issue