로그시스템 개선

This commit is contained in:
kjs
2025-10-27 11:11:08 +09:00
parent f14d9ee66c
commit 5fdefffd26
12 changed files with 1588 additions and 93 deletions

View File

@@ -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"> . &quot;id&quot;.</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">
&quot;ID 123 &quot; &quot; &quot;
</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> &quot; ID: 5&quot;
&quot;DTG-001 (ID: 5)&quot; .
<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>
);
};