From addff4769ba7815c7ea6de70f289a4549395c9ec Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 24 Oct 2025 16:39:54 +0900 Subject: [PATCH] =?UTF-8?q?api=EC=9A=94=EC=B2=AD=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../properties/TableSourceProperties.tsx | 314 +++++++++--------- .../FlowVisibilityConfigPanel.tsx | 43 ++- .../components/screen/widgets/FlowWidget.tsx | 27 +- frontend/lib/api/flow.ts | 28 +- 4 files changed, 217 insertions(+), 195 deletions(-) diff --git a/frontend/components/dataflow/node-editor/panels/properties/TableSourceProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/TableSourceProperties.tsx index a342a213..9342e583 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/TableSourceProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/TableSourceProperties.tsx @@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import { tableTypeApi } from "@/lib/api/screen"; @@ -34,10 +35,10 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro const [displayName, setDisplayName] = useState(data.displayName || data.tableName); const [tableName, setTableName] = useState(data.tableName); - + // πŸ†• 데이터 μ†ŒμŠ€ νƒ€μž… (κΈ°λ³Έκ°’: context-data) const [dataSourceType, setDataSourceType] = useState<"context-data" | "table-all">( - (data as any).dataSourceType || "context-data" + (data as any).dataSourceType || "context-data", ); // ν…Œμ΄λΈ” 선택 κ΄€λ ¨ μƒνƒœ @@ -167,171 +168,168 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro return (
- {/* κΈ°λ³Έ 정보 */} -
-

κΈ°λ³Έ 정보

+ {/* κΈ°λ³Έ 정보 */} +
+

κΈ°λ³Έ 정보

-
-
- - handleDisplayNameChange(e.target.value)} - className="mt-1" - placeholder="λ…Έλ“œ ν‘œμ‹œ 이름" - /> -
+
+
+ + handleDisplayNameChange(e.target.value)} + className="mt-1" + placeholder="λ…Έλ“œ ν‘œμ‹œ 이름" + /> +
- {/* ν…Œμ΄λΈ” 선택 Combobox */} -
- - - - - - - - - - 검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. - - - {tables.map((table) => ( - handleTableSelect(table.tableName)} - className="cursor-pointer" - > - -
- {table.label} - {table.label !== table.tableName && ( - {table.tableName} - )} - {table.description && ( - {table.description} - )} -
-
- ))} -
-
-
-
-
-
- {tableName && selectedTableLabel !== tableName && ( -

- μ‹€μ œ ν…Œμ΄λΈ”λͺ…: {tableName} -

+ {/* ν…Œμ΄λΈ” 선택 Combobox */} +
+ + + + + + + + + + 검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. + + + {tables.map((table) => ( + handleTableSelect(table.tableName)} + className="cursor-pointer" + > + +
+ {table.label} + {table.label !== table.tableName && ( + {table.tableName} + )} + {table.description && ( + {table.description} + )} +
+
+ ))} +
+
+
+
+
+
+ {tableName && selectedTableLabel !== tableName && ( +

+ μ‹€μ œ ν…Œμ΄λΈ”λͺ…: {tableName} +

+ )} +
+
+
+ + {/* πŸ†• 데이터 μ†ŒμŠ€ μ„€μ • */} +
+

데이터 μ†ŒμŠ€ μ„€μ •

+ +
+
+ + + + {/* μ„€λͺ… ν…μŠ€νŠΈ */} +
+ {dataSourceType === "context-data" ? ( + <> +

πŸ’‘ μ»¨ν…μŠ€νŠΈ 데이터 λͺ¨λ“œ

+

λ²„νŠΌ μ‹€ν–‰ μ‹œ μ „λ‹¬λœ 데이터(폼 데이터, ν…Œμ΄λΈ” 선택 ν•­λͺ© λ“±)λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

+

β€’ 폼 데이터: 1개 λ ˆμ½”λ“œ

+

β€’ ν…Œμ΄λΈ” 선택: N개 λ ˆμ½”λ“œ

+ + ) : ( + <> +

πŸ“Š ν…Œμ΄λΈ” 전체 데이터 λͺ¨λ“œ

+

μ„ νƒν•œ ν…Œμ΄λΈ”μ˜ **λͺ¨λ“  ν–‰**을 직접 μ‘°νšŒν•©λ‹ˆλ‹€.

+

⚠️ λŒ€λŸ‰ 데이터 μ‹œ μ„±λŠ₯ 주의

+ )}
+
- {/* πŸ†• 데이터 μ†ŒμŠ€ μ„€μ • */} -
-

데이터 μ†ŒμŠ€ μ„€μ •

- -
-
- - - - {/* μ„€λͺ… ν…μŠ€νŠΈ */} -
- {dataSourceType === "context-data" ? ( - <> -

πŸ’‘ μ»¨ν…μŠ€νŠΈ 데이터 λͺ¨λ“œ

-

λ²„νŠΌ μ‹€ν–‰ μ‹œ μ „λ‹¬λœ 데이터(폼 데이터, ν…Œμ΄λΈ” 선택 ν•­λͺ© λ“±)λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

-

β€’ 폼 데이터: 1개 λ ˆμ½”λ“œ

-

β€’ ν…Œμ΄λΈ” 선택: N개 λ ˆμ½”λ“œ

- - ) : ( - <> -

πŸ“Š ν…Œμ΄λΈ” 전체 데이터 λͺ¨λ“œ

-

μ„ νƒν•œ ν…Œμ΄λΈ”μ˜ **λͺ¨λ“  ν–‰**을 직접 μ‘°νšŒν•©λ‹ˆλ‹€.

-

⚠️ λŒ€λŸ‰ 데이터 μ‹œ μ„±λŠ₯ 주의

- - )} + {/* ν•„λ“œ 정보 */} +
+

+ 좜λ ₯ ν•„λ“œ {data.fields && data.fields.length > 0 && `(${data.fields.length}개)`} +

+ {data.fields && data.fields.length > 0 ? ( +
+ {data.fields.map((field) => ( +
+ + {field.name} + + {field.type}
-
+ ))}
-
- - {/* ν•„λ“œ 정보 */} -
-

- 좜λ ₯ ν•„λ“œ {data.fields && data.fields.length > 0 && `(${data.fields.length}개)`} -

- {data.fields && data.fields.length > 0 ? ( -
- {data.fields.map((field) => ( -
- - {field.name} - - {field.type} -
- ))} -
- ) : ( -
ν•„λ“œ 정보가 μ—†μŠ΅λ‹ˆλ‹€
- )} -
- + ) : ( +
ν•„λ“œ 정보가 μ—†μŠ΅λ‹ˆλ‹€
+ )} +
); } diff --git a/frontend/components/screen/config-panels/FlowVisibilityConfigPanel.tsx b/frontend/components/screen/config-panels/FlowVisibilityConfigPanel.tsx index 9d0f859f..d820f2ca 100644 --- a/frontend/components/screen/config-panels/FlowVisibilityConfigPanel.tsx +++ b/frontend/components/screen/config-panels/FlowVisibilityConfigPanel.tsx @@ -13,7 +13,7 @@ import { Input } from "@/components/ui/input"; import { Workflow, Info, CheckCircle, XCircle, Loader2, ArrowRight, ArrowDown } from "lucide-react"; import { ComponentData } from "@/types/screen"; import { FlowVisibilityConfig } from "@/types/control-management"; -import { getFlowById } from "@/lib/api/flow"; +import { getFlowById, getFlowSteps } from "@/lib/api/flow"; import type { FlowDefinition, FlowStep } from "@/types/flow"; import { toast } from "sonner"; @@ -25,7 +25,7 @@ interface FlowVisibilityConfigPanelProps { /** * ν”Œλ‘œμš° 단계별 λ²„νŠΌ ν‘œμ‹œ μ„€μ • νŒ¨λ„ - * + * * ν”Œλ‘œμš° μœ„μ ―μ΄ 화면에 μžˆμ„ λ•Œ, λ²„νŠΌμ΄ νŠΉμ • ν”Œλ‘œμš° λ‹¨κ³„μ—μ„œλ§Œ ν‘œμ‹œλ˜λ„λ‘ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. */ export const FlowVisibilityConfigPanel: React.FC = ({ @@ -40,8 +40,7 @@ export const FlowVisibilityConfigPanel: React.FC const flowWidgets = useMemo(() => { return allComponents.filter((comp) => { const isFlowWidget = - comp.type === "flow" || - (comp.type === "component" && (comp as any).componentConfig?.type === "flow-widget"); + comp.type === "flow" || (comp.type === "component" && (comp as any).componentConfig?.type === "flow-widget"); return isFlowWidget; }); }, [allComponents]); @@ -49,23 +48,23 @@ export const FlowVisibilityConfigPanel: React.FC // State const [enabled, setEnabled] = useState(currentConfig?.enabled || false); const [selectedFlowComponentId, setSelectedFlowComponentId] = useState( - currentConfig?.targetFlowComponentId || null + currentConfig?.targetFlowComponentId || null, ); const [mode, setMode] = useState<"whitelist" | "blacklist" | "all">(currentConfig?.mode || "whitelist"); const [visibleSteps, setVisibleSteps] = useState(currentConfig?.visibleSteps || []); const [hiddenSteps, setHiddenSteps] = useState(currentConfig?.hiddenSteps || []); const [layoutBehavior, setLayoutBehavior] = useState<"preserve-position" | "auto-compact">( - currentConfig?.layoutBehavior || "auto-compact" + currentConfig?.layoutBehavior || "auto-compact", ); // πŸ†• κ·Έλ£Ή μ„€μ • (auto-compact λͺ¨λ“œμ—μ„œλ§Œ μ‚¬μš©) const [groupId, setGroupId] = useState(currentConfig?.groupId || `group-${Date.now()}`); const [groupDirection, setGroupDirection] = useState<"horizontal" | "vertical">( - currentConfig?.groupDirection || "horizontal" + currentConfig?.groupDirection || "horizontal", ); const [groupGap, setGroupGap] = useState(currentConfig?.groupGap ?? 8); const [groupAlign, setGroupAlign] = useState<"start" | "center" | "end" | "space-between" | "space-around">( - currentConfig?.groupAlign || "start" + currentConfig?.groupAlign || "start", ); // μ„ νƒλœ ν”Œλ‘œμš°μ˜ μŠ€ν… λͺ©λ‘ @@ -127,13 +126,12 @@ export const FlowVisibilityConfigPanel: React.FC setFlowInfo(flowResponse.data); // μŠ€ν… λͺ©λ‘ 쑰회 - const stepsResponse = await fetch(`/api/flow/definitions/${flowId}/steps`); - if (!stepsResponse.ok) { + const stepsResponse = await getFlowSteps(flowId); + if (!stepsResponse.success) { throw new Error("μŠ€ν… λͺ©λ‘μ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€"); } - const stepsData = await stepsResponse.json(); - if (stepsData.success && stepsData.data) { - const sortedSteps = stepsData.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder); + if (stepsResponse.data) { + const sortedSteps = stepsResponse.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder); setFlowSteps(sortedSteps); } } catch (error: any) { @@ -346,12 +344,10 @@ export const FlowVisibilityConfigPanel: React.FC
{/* μŠ€ν… μ²΄ν¬λ°•μŠ€ λͺ©λ‘ */} -
+
{flowSteps.map((step) => { const isChecked = - mode === "whitelist" - ? visibleSteps.includes(step.id) - : hiddenSteps.includes(step.id); + mode === "whitelist" ? visibleSteps.includes(step.id) : hiddenSteps.includes(step.id); return (
@@ -366,7 +362,9 @@ export const FlowVisibilityConfigPanel: React.FC {step.stepName} {isChecked && ( - + )}
@@ -403,14 +401,12 @@ export const FlowVisibilityConfigPanel: React.FC {/* πŸ†• κ·Έλ£Ή μ„€μ • (auto-compact λͺ¨λ“œμΌ λ•Œλ§Œ ν‘œμ‹œ) */} {layoutBehavior === "auto-compact" && ( -
+
κ·Έλ£Ή μ„€μ • -

- 같은 κ·Έλ£Ή IDλ₯Ό κ°€μ§„ λ²„νŠΌλ“€μ΄ μžλ™μœΌλ‘œ μ •λ ¬λ©λ‹ˆλ‹€ -

+

같은 κ·Έλ£Ή IDλ₯Ό κ°€μ§„ λ²„νŠΌλ“€μ΄ μžλ™μœΌλ‘œ μ •λ ¬λ©λ‹ˆλ‹€

{/* κ·Έλ£Ή ID */} @@ -425,7 +421,7 @@ export const FlowVisibilityConfigPanel: React.FC placeholder="group-1" className="h-8 text-xs sm:h-9 sm:text-sm" /> -

+

같은 κ·Έλ£Ή IDλ₯Ό κ°€μ§„ λ²„νŠΌλ“€μ΄ ν•˜λ‚˜μ˜ 그룹으둜 λ¬Άμž…λ‹ˆλ‹€

@@ -577,4 +573,3 @@ export const FlowVisibilityConfigPanel: React.FC ); }; - diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index 78156945..1204ccdb 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -5,7 +5,14 @@ import { FlowComponent } from "@/types/screen-management"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { AlertCircle, Loader2, ChevronUp, History } from "lucide-react"; -import { getFlowById, getAllStepCounts, getStepDataList, getFlowAuditLogs } from "@/lib/api/flow"; +import { + getFlowById, + getAllStepCounts, + getStepDataList, + getFlowAuditLogs, + getFlowSteps, + getFlowConnections, +} from "@/lib/api/flow"; import type { FlowDefinition, FlowStep, FlowAuditLog } from "@/types/flow"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; @@ -162,22 +169,18 @@ export function FlowWidget({ setFlowData(flowResponse.data); // μŠ€ν… λͺ©λ‘ 쑰회 - const stepsResponse = await fetch(`/api/flow/definitions/${flowId}/steps`); - if (!stepsResponse.ok) { + const stepsResponse = await getFlowSteps(flowId); + if (!stepsResponse.success) { throw new Error("μŠ€ν… λͺ©λ‘μ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€"); } - const stepsData = await stepsResponse.json(); - if (stepsData.success && stepsData.data) { - const sortedSteps = stepsData.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder); + if (stepsResponse.data) { + const sortedSteps = stepsResponse.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder); setSteps(sortedSteps); // μ—°κ²° 정보 쑰회 - const connectionsResponse = await fetch(`/api/flow/connections/${flowId}`); - if (connectionsResponse.ok) { - const connectionsData = await connectionsResponse.json(); - if (connectionsData.success && connectionsData.data) { - setConnections(connectionsData.data); - } + const connectionsResponse = await getFlowConnections(flowId); + if (connectionsResponse.success && connectionsResponse.data) { + setConnections(connectionsResponse.data); } // μŠ€ν…λ³„ 데이터 건수 쑰회 diff --git a/frontend/lib/api/flow.ts b/frontend/lib/api/flow.ts index 7d61293a..d94455ff 100644 --- a/frontend/lib/api/flow.ts +++ b/frontend/lib/api/flow.ts @@ -19,7 +19,33 @@ import { ApiResponse, } from "@/types/flow"; -const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api"; +// API URL 동적 μ„€μ • +const getApiBaseUrl = (): string => { + // 1. ν™˜κ²½λ³€μˆ˜κ°€ 있으면 μš°μ„  μ‚¬μš© + if (process.env.NEXT_PUBLIC_API_URL) { + return process.env.NEXT_PUBLIC_API_URL; + } + + // 2. ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œμ—μ„œ 동적 μ„€μ • + if (typeof window !== "undefined") { + const currentHost = window.location.hostname; + + // ν”„λ‘œλ•μ…˜ ν™˜κ²½: v1.vexplor.com β†’ api.vexplor.com + if (currentHost === "v1.vexplor.com") { + return "https://api.vexplor.com/api"; + } + + // 둜컬 κ°œλ°œν™˜κ²½ + if (currentHost === "localhost" || currentHost === "127.0.0.1") { + return "http://localhost:8080/api"; + } + } + + // 3. κΈ°λ³Έκ°’ + return "/api"; +}; + +const API_BASE = getApiBaseUrl(); // 토큰 κ°€μ Έμ˜€κΈ° function getAuthToken(): string | null {