로그시스템 개선
This commit is contained in:
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, ChevronsUpDown, Search } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -19,6 +20,7 @@ interface ButtonConfigPanelProps {
|
||||
component: ComponentData;
|
||||
onUpdateProperty: (path: string, value: any) => void;
|
||||
allComponents?: ComponentData[]; // 🆕 플로우 위젯 감지용
|
||||
currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용)
|
||||
}
|
||||
|
||||
interface ScreenOption {
|
||||
@@ -27,20 +29,23 @@ interface ScreenOption {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
component,
|
||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
component,
|
||||
onUpdateProperty,
|
||||
allComponents = [], // 🆕 기본값 빈 배열
|
||||
currentTableName, // 현재 화면의 테이블명
|
||||
}) => {
|
||||
console.log("🎨 ButtonConfigPanel 렌더링:", {
|
||||
componentId: component.id,
|
||||
"component.componentConfig?.action?.type": component.componentConfig?.action?.type,
|
||||
});
|
||||
|
||||
// 🔧 component에서 직접 읽기 (useMemo 제거)
|
||||
const config = component.componentConfig || {};
|
||||
const currentAction = component.componentConfig?.action || {};
|
||||
|
||||
console.log("🎨 ButtonConfigPanel 렌더링:", {
|
||||
componentId: component.id,
|
||||
"component.componentConfig?.action?.type": component.componentConfig?.action?.type,
|
||||
currentTableName: currentTableName,
|
||||
"config.action?.historyTableName": config.action?.historyTableName,
|
||||
});
|
||||
|
||||
// 로컬 상태 관리 (실시간 입력 반영)
|
||||
const [localInputs, setLocalInputs] = useState({
|
||||
text: config.text !== undefined ? config.text : "버튼",
|
||||
@@ -57,6 +62,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
||||
const [navSearchTerm, setNavSearchTerm] = useState("");
|
||||
|
||||
// 테이블 컬럼 목록 상태
|
||||
const [tableColumns, setTableColumns] = useState<string[]>([]);
|
||||
const [columnsLoading, setColumnsLoading] = useState(false);
|
||||
const [displayColumnOpen, setDisplayColumnOpen] = useState(false);
|
||||
const [displayColumnSearch, setDisplayColumnSearch] = useState("");
|
||||
|
||||
// 컴포넌트 prop 변경 시 로컬 상태 동기화 (Input만)
|
||||
useEffect(() => {
|
||||
const latestConfig = component.componentConfig || {};
|
||||
@@ -103,6 +114,115 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
fetchScreens();
|
||||
}, []);
|
||||
|
||||
// 테이블 컬럼 목록 가져오기 (테이블 이력 보기 액션일 때)
|
||||
useEffect(() => {
|
||||
const fetchTableColumns = async () => {
|
||||
// 테이블 이력 보기 액션이 아니면 스킵
|
||||
if (config.action?.type !== "view_table_history") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 수동 입력된 테이블명 우선
|
||||
// 2. 없으면 현재 화면의 테이블명 사용
|
||||
const tableName = config.action?.historyTableName || currentTableName;
|
||||
|
||||
// 테이블명이 없으면 스킵
|
||||
if (!tableName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setColumnsLoading(true);
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`, {
|
||||
params: {
|
||||
page: 1,
|
||||
size: 9999, // 전체 컬럼 가져오기
|
||||
},
|
||||
});
|
||||
|
||||
console.log("📋 [ButtonConfigPanel] API 응답:", {
|
||||
tableName,
|
||||
success: response.data.success,
|
||||
hasData: !!response.data.data,
|
||||
hasColumns: !!response.data.data?.columns,
|
||||
totalColumns: response.data.data?.columns?.length,
|
||||
});
|
||||
|
||||
// API 응답 구조: { success, data: { columns: [...], total, page, totalPages } }
|
||||
const columnData = response.data.data?.columns;
|
||||
|
||||
if (!columnData || !Array.isArray(columnData)) {
|
||||
console.error("❌ 컬럼 데이터가 배열이 아닙니다:", columnData);
|
||||
setTableColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data.success) {
|
||||
// ID 컬럼과 날짜 관련 컬럼 제외
|
||||
const filteredColumns = columnData
|
||||
.filter((col: any) => {
|
||||
const colName = col.columnName.toLowerCase();
|
||||
const dataType = col.dataType?.toLowerCase() || "";
|
||||
|
||||
console.log(`🔍 [필터링 체크] ${col.columnName}:`, {
|
||||
colName,
|
||||
dataType,
|
||||
isId: colName === "id" || colName.endsWith("_id"),
|
||||
hasDateInType: dataType.includes("date") || dataType.includes("time") || dataType.includes("timestamp"),
|
||||
hasDateInName:
|
||||
colName.includes("date") ||
|
||||
colName.includes("time") ||
|
||||
colName.endsWith("_at") ||
|
||||
colName.startsWith("created") ||
|
||||
colName.startsWith("updated"),
|
||||
});
|
||||
|
||||
// ID 컬럼 제외 (id, _id로 끝나는 컬럼)
|
||||
if (colName === "id" || colName.endsWith("_id")) {
|
||||
console.log(` ❌ 제외: ID 컬럼`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 날짜/시간 타입 제외 (데이터 타입 기준)
|
||||
if (dataType.includes("date") || dataType.includes("time") || dataType.includes("timestamp")) {
|
||||
console.log(` ❌ 제외: 날짜/시간 타입`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 날짜/시간 관련 컬럼명 제외 (컬럼명에 date, time, at 포함)
|
||||
if (
|
||||
colName.includes("date") ||
|
||||
colName.includes("time") ||
|
||||
colName.endsWith("_at") ||
|
||||
colName.startsWith("created") ||
|
||||
colName.startsWith("updated")
|
||||
) {
|
||||
console.log(` ❌ 제외: 날짜 관련 컬럼명`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(` ✅ 통과`);
|
||||
return true;
|
||||
})
|
||||
.map((col: any) => col.columnName);
|
||||
|
||||
console.log("✅ [ButtonConfigPanel] 필터링된 컬럼:", {
|
||||
totalFiltered: filteredColumns.length,
|
||||
columns: filteredColumns,
|
||||
});
|
||||
|
||||
setTableColumns(filteredColumns);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 테이블 컬럼 로딩 실패:", error);
|
||||
} finally {
|
||||
setColumnsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTableColumns();
|
||||
}, [config.action?.type, config.action?.historyTableName, currentTableName]);
|
||||
|
||||
// 검색 필터링 함수
|
||||
const filterScreens = (searchTerm: string) => {
|
||||
if (!searchTerm.trim()) return screens;
|
||||
@@ -185,20 +305,20 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
key={`action-${component.id}-${component.componentConfig?.action?.type || "save"}`}
|
||||
value={component.componentConfig?.action?.type || "save"}
|
||||
onValueChange={(value) => {
|
||||
console.log("🎯 버튼 액션 드롭다운 변경:", {
|
||||
oldValue: component.componentConfig?.action?.type,
|
||||
newValue: value,
|
||||
});
|
||||
|
||||
// 🔥 action.type 업데이트
|
||||
onUpdateProperty("componentConfig.action.type", value);
|
||||
|
||||
// 🔥 색상 업데이트는 충분히 지연 (React 리렌더링 완료 후)
|
||||
setTimeout(() => {
|
||||
const newColor = value === "delete" ? "#ef4444" : "#212121";
|
||||
console.log("🎨 라벨 색상 업데이트:", { value, newColor });
|
||||
onUpdateProperty("style.labelColor", newColor);
|
||||
}, 100); // 0 → 100ms로 증가
|
||||
console.log("🎯 버튼 액션 드롭다운 변경:", {
|
||||
oldValue: component.componentConfig?.action?.type,
|
||||
newValue: value,
|
||||
});
|
||||
|
||||
// 🔥 action.type 업데이트
|
||||
onUpdateProperty("componentConfig.action.type", value);
|
||||
|
||||
// 🔥 색상 업데이트는 충분히 지연 (React 리렌더링 완료 후)
|
||||
setTimeout(() => {
|
||||
const newColor = value === "delete" ? "#ef4444" : "#212121";
|
||||
console.log("🎨 라벨 색상 업데이트:", { value, newColor });
|
||||
onUpdateProperty("style.labelColor", newColor);
|
||||
}, 100); // 0 → 100ms로 증가
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -211,6 +331,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||
<SelectItem value="modal">모달 열기</SelectItem>
|
||||
<SelectItem value="control">제어 흐름</SelectItem>
|
||||
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -476,6 +597,162 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 테이블 이력 보기 액션 설정 */}
|
||||
{(component.componentConfig?.action?.type || "save") === "view_table_history" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-blue-50 p-4">
|
||||
<h4 className="text-sm font-medium text-blue-900">📜 테이블 이력 보기 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="history-table-name">테이블명 (선택사항)</Label>
|
||||
<Input
|
||||
id="history-table-name"
|
||||
placeholder="자동 감지 (비워두면 현재 화면의 테이블 사용)"
|
||||
value={config.action?.historyTableName || ""}
|
||||
onChange={(e) => {
|
||||
onUpdateProperty("componentConfig.action.historyTableName", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-600">비워두면 현재 화면의 테이블을 자동으로 사용합니다</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="history-record-id-field">레코드 ID 필드명</Label>
|
||||
<Input
|
||||
id="history-record-id-field"
|
||||
placeholder="id (기본값)"
|
||||
value={config.action?.historyRecordIdField || ""}
|
||||
onChange={(e) => {
|
||||
onUpdateProperty("componentConfig.action.historyRecordIdField", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-600">기본키 컬럼명입니다. 대부분 "id"입니다.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="history-record-id-source">레코드 ID 가져올 위치</Label>
|
||||
<Select
|
||||
value={config.action?.historyRecordIdSource || "selected_row"}
|
||||
onValueChange={(value) => {
|
||||
onUpdateProperty("componentConfig.action.historyRecordIdSource", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="레코드 ID 소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="selected_row">선택된 행 (권장)</SelectItem>
|
||||
<SelectItem value="form_field">폼 필드</SelectItem>
|
||||
<SelectItem value="context">컨텍스트 (원본 데이터)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-gray-600">테이블 리스트에서 선택된 행의 ID를 사용합니다</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="history-record-label-field">레코드 라벨 필드 (선택사항)</Label>
|
||||
<Input
|
||||
id="history-record-label-field"
|
||||
placeholder="예: name, title, device_code 등"
|
||||
value={config.action?.historyRecordLabelField || ""}
|
||||
onChange={(e) => {
|
||||
onUpdateProperty("componentConfig.action.historyRecordLabelField", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-600">
|
||||
이력 모달에서 "ID 123의 이력" 대신 "홍길동의 이력" 처럼 표시할 때 사용
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<Label className="text-blue-900">
|
||||
전체 이력 표시 컬럼 (필수) <span className="text-red-600">*</span>
|
||||
</Label>
|
||||
|
||||
{!config.action?.historyTableName && !currentTableName ? (
|
||||
<div className="mt-2 rounded-md border border-yellow-300 bg-yellow-50 p-3">
|
||||
<p className="text-xs text-yellow-800">
|
||||
⚠️ 먼저 <strong>테이블명</strong>을 입력하거나, 현재 화면에 테이블을 연결해주세요.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!config.action?.historyTableName && currentTableName && (
|
||||
<div className="mt-2 rounded-md border border-green-300 bg-green-50 p-2">
|
||||
<p className="text-xs text-green-800">
|
||||
✓ 현재 화면의 테이블 <strong>{currentTableName}</strong>을(를) 자동으로 사용합니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Popover open={displayColumnOpen} onOpenChange={setDisplayColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={displayColumnOpen}
|
||||
className="mt-2 h-10 w-full justify-between text-sm"
|
||||
disabled={columnsLoading || tableColumns.length === 0}
|
||||
>
|
||||
{columnsLoading
|
||||
? "로딩 중..."
|
||||
: config.action?.historyDisplayColumn
|
||||
? config.action.historyDisplayColumn
|
||||
: tableColumns.length === 0
|
||||
? "사용 가능한 컬럼이 없습니다"
|
||||
: "컬럼을 선택하세요"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-sm">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tableColumns.map((column) => (
|
||||
<CommandItem
|
||||
key={column}
|
||||
value={column}
|
||||
onSelect={(currentValue) => {
|
||||
onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue);
|
||||
setDisplayColumnOpen(false);
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
config.action?.historyDisplayColumn === column ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{column}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<p className="mt-2 text-xs text-gray-700">
|
||||
<strong>전체 테이블 이력</strong>에서 레코드를 구분하기 위한 컬럼입니다.
|
||||
<br />
|
||||
예: <code className="rounded bg-white px-1">device_code</code>를 설정하면 "레코드 ID: 5"
|
||||
대신 "DTG-001 (ID: 5)"로 표시됩니다.
|
||||
<br />이 컬럼으로 검색도 가능합니다.
|
||||
</p>
|
||||
|
||||
{tableColumns.length === 0 && !columnsLoading && (
|
||||
<p className="mt-2 text-xs text-red-600">
|
||||
⚠️ ID 및 날짜 타입 컬럼을 제외한 사용 가능한 컬럼이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 페이지 이동 액션 설정 */}
|
||||
{(component.componentConfig?.action?.type || "save") === "navigate" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||
@@ -580,13 +857,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
|
||||
{/* 🆕 플로우 단계별 표시 제어 섹션 */}
|
||||
<div className="mt-8 border-t border-gray-200 pt-6">
|
||||
<FlowVisibilityConfigPanel
|
||||
component={component}
|
||||
allComponents={allComponents}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
<FlowVisibilityConfigPanel
|
||||
component={component}
|
||||
allComponents={allComponents}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user