From 25cd23c1fbf9bb3afa347d69224fb5e74ace3813 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 24 Oct 2025 16:34:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B9=84=EC=9C=A8?= =?UTF-8?q?=EC=A1=B0=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 43 +-- frontend/components/layout/AppLayout.tsx | 2 +- .../screen/InteractiveScreenViewer.tsx | 2 +- .../screen/RealtimePreviewDynamic.tsx | 28 +- .../components/screen/widgets/FlowWidget.tsx | 330 ++++++++++-------- 5 files changed, 218 insertions(+), 187 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 21987359..d365ebbd 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -52,9 +52,8 @@ export default function ScreenViewPage() { modalDescription?: string; }>({}); - // 자동 스케일 조정 (사용자 화면 크기에 맞춤) - const [scale, setScale] = useState(1); const containerRef = React.useRef(null); + const [scale, setScale] = useState(1); useEffect(() => { const initComponents = async () => { @@ -140,32 +139,37 @@ export default function ScreenViewPage() { } }, [screenId]); - // 자동 스케일 조정 useEffect (항상 화면에 꽉 차게) + // 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) useEffect(() => { const updateScale = () => { if (containerRef.current && layout) { - const screenWidth = layout?.screenResolution?.width || 1200; + const designWidth = layout?.screenResolution?.width || 1200; + const designHeight = layout?.screenResolution?.height || 800; + const containerWidth = containerRef.current.offsetWidth; - const availableWidth = containerWidth - 32; // 좌우 패딩 16px * 2 + const containerHeight = containerRef.current.offsetHeight; - // 항상 화면에 맞춰서 스케일 조정 (늘리거나 줄임) - const newScale = availableWidth / screenWidth; + // 가로/세로 비율 중 작은 것을 선택 (화면에 맞게) + const scaleX = containerWidth / designWidth; + const scaleY = containerHeight / designHeight; + const newScale = Math.min(scaleX, scaleY); - console.log("📏 스케일 계산 (화면 꽉 차게):", { - screenWidth, + console.log("📏 캔버스 스케일 계산:", { + designWidth, + designHeight, containerWidth, - availableWidth, - scale: newScale, + containerHeight, + scaleX, + scaleY, + finalScale: newScale, }); setScale(newScale); } }; - // 초기 측정 (DOM이 완전히 렌더링된 후) - const timer = setTimeout(() => { - updateScale(); - }, 100); + // 초기 측정 + const timer = setTimeout(updateScale, 100); window.addEventListener("resize", updateScale); return () => { @@ -207,17 +211,16 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
+
{/* 절대 위치 기반 렌더링 */} {layout && layout.components.length > 0 ? (
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */} diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 71447d4f..cafd611a 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -334,7 +334,7 @@ export const InteractiveScreenViewer: React.FC = ( console.log("🔍 InteractiveScreenViewer 최종 flowComponent:", flowComponent); return ( -
+
); diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index a00f972f..0cc6ca7f 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -34,7 +34,7 @@ interface RealtimePreviewProps { onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 onConfigChange?: (config: any) => void; // 설정 변경 핸들러 - + // 버튼 액션을 위한 props screenId?: number; tableName?: string; @@ -47,7 +47,7 @@ interface RealtimePreviewProps { onRefresh?: () => void; flowRefreshKey?: number; onFlowRefresh?: () => void; - + // 폼 데이터 관련 props formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; @@ -115,24 +115,24 @@ export const RealtimePreviewDynamic: React.FC = ({ // 플로우 위젯의 실제 높이 측정 React.useEffect(() => { const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget"; - + if (isFlowWidget && contentRef.current) { const measureHeight = () => { if (contentRef.current) { // getBoundingClientRect()로 실제 렌더링된 높이 측정 const rect = contentRef.current.getBoundingClientRect(); const measured = rect.height; - + // scrollHeight도 함께 확인하여 더 큰 값 사용 const scrollHeight = contentRef.current.scrollHeight; const rawHeight = Math.max(measured, scrollHeight); - + // 40px 단위로 올림 const finalHeight = Math.ceil(rawHeight / 40) * 40; - + if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) { setActualHeight(finalHeight); - + // 컴포넌트의 실제 size.height도 업데이트 (중복 업데이트 방지) if (onConfigChange && finalHeight !== lastUpdatedHeight.current && finalHeight !== component.size?.height) { lastUpdatedHeight.current = finalHeight; @@ -142,11 +142,11 @@ export const RealtimePreviewDynamic: React.FC = ({ newHeight: finalHeight, }); // size는 별도 속성이므로 직접 업데이트 - const event = new CustomEvent('updateComponentSize', { + const event = new CustomEvent("updateComponentSize", { detail: { componentId: component.id, - height: finalHeight - } + height: finalHeight, + }, }); window.dispatchEvent(event); } @@ -276,10 +276,10 @@ export const RealtimePreviewDynamic: React.FC = ({ > {/* 동적 컴포넌트 렌더링 */}
([]); @@ -385,7 +386,7 @@ export function FlowWidget({ : "flex flex-col items-center gap-4"; return ( -
+
{/* 플로우 제목 */}
@@ -647,8 +648,8 @@ export function FlowWidget({ {/* 선택된 스텝의 데이터 리스트 */} {selectedStepId !== null && ( -
- {/* 헤더 */} +
+ {/* 헤더 - 자동 높이 */}

{steps.find((s) => s.id === selectedStepId)?.stepName} @@ -661,34 +662,34 @@ export function FlowWidget({

- {/* 데이터 영역 - 스크롤 가능 */} -
- {stepDataLoading ? ( -
- - 데이터 로딩 중... -
- ) : stepData.length === 0 ? ( -
- - - - 데이터가 없습니다 -
- ) : ( - <> - {/* 모바일: 카드 뷰 */} -
+ {/* 데이터 영역 - 고정 높이 + 스크롤 */} + {stepDataLoading ? ( +
+ + 데이터 로딩 중... +
+ ) : stepData.length === 0 ? ( +
+ + + + 데이터가 없습니다 +
+ ) : ( + <> + {/* 모바일: 카드 뷰 - 고정 높이 + 스크롤 */} +
+
{paginatedStepData.map((row, pageIndex) => { const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; return ( @@ -725,132 +726,159 @@ export function FlowWidget({ ); })}
+
- {/* 데스크톱: 테이블 뷰 */} -
- - - - {allowDataMove && ( - - 0} - onCheckedChange={toggleAllRows} - /> - - )} - {stepDataColumns.map((col) => ( - - {col} - - ))} - - - - {paginatedStepData.map((row, pageIndex) => { - const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; - return ( - - {allowDataMove && ( - - toggleRowSelection(actualIndex)} - /> - - )} - {stepDataColumns.map((col) => ( - - {row[col] !== null && row[col] !== undefined ? ( - String(row[col]) - ) : ( - - - )} - - ))} - - ); - })} - -
-
- - )} -
+ {/* 데스크톱: 테이블 뷰 - 고정 높이 + 스크롤 */} +
+ + + + {allowDataMove && ( + + 0} + onCheckedChange={toggleAllRows} + /> + + )} + {stepDataColumns.map((col) => ( + + {col} + + ))} + + + + {paginatedStepData.map((row, pageIndex) => { + const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; + return ( + + {allowDataMove && ( + + toggleRowSelection(actualIndex)} + /> + + )} + {stepDataColumns.map((col) => ( + + {row[col] !== null && row[col] !== undefined ? ( + String(row[col]) + ) : ( + - + )} + + ))} + + ); + })} + +
+
+ + )} - {/* 페이지네이션 푸터 */} - {!stepDataLoading && stepData.length > 0 && totalStepDataPages > 1 && ( + {/* 페이지네이션 - 항상 하단에 고정 */} + {!stepDataLoading && stepData.length > 0 && (
-
- 페이지 {stepDataPage} / {totalStepDataPages} (총 {stepData.length}건) + {/* 왼쪽: 페이지 정보 + 페이지 크기 선택 */} +
+
+ 페이지 {stepDataPage} / {totalStepDataPages} (총 {stepData.length}건) +
+
+ 표시 개수: + +
- - - - setStepDataPage((p) => Math.max(1, p - 1))} - className={stepDataPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} - /> - - {totalStepDataPages <= 7 ? ( - Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => ( - - setStepDataPage(page)} - isActive={stepDataPage === page} - className="cursor-pointer" - > - {page} - - - )) - ) : ( - <> - {Array.from({ length: totalStepDataPages }, (_, i) => i + 1) - .filter((page) => { - return ( - page === 1 || - page === totalStepDataPages || - (page >= stepDataPage - 2 && page <= stepDataPage + 2) - ); - }) - .map((page, idx, arr) => ( - - {idx > 0 && arr[idx - 1] !== page - 1 && ( + + {/* 오른쪽: 페이지네이션 */} + {totalStepDataPages > 1 && ( + + + + setStepDataPage((p) => Math.max(1, p - 1))} + className={stepDataPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + {totalStepDataPages <= 7 ? ( + Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => ( + + setStepDataPage(page)} + isActive={stepDataPage === page} + className="cursor-pointer" + > + {page} + + + )) + ) : ( + <> + {Array.from({ length: totalStepDataPages }, (_, i) => i + 1) + .filter((page) => { + return ( + page === 1 || + page === totalStepDataPages || + (page >= stepDataPage - 2 && page <= stepDataPage + 2) + ); + }) + .map((page, idx, arr) => ( + + {idx > 0 && arr[idx - 1] !== page - 1 && ( + + ... + + )} - ... + setStepDataPage(page)} + isActive={stepDataPage === page} + className="cursor-pointer" + > + {page} + - )} - - setStepDataPage(page)} - isActive={stepDataPage === page} - className="cursor-pointer" - > - {page} - - - - ))} - - )} - - setStepDataPage((p) => Math.min(totalStepDataPages, p + 1))} - className={ - stepDataPage === totalStepDataPages ? "pointer-events-none opacity-50" : "cursor-pointer" - } - /> - - - + + ))} + + )} + + setStepDataPage((p) => Math.min(totalStepDataPages, p + 1))} + className={ + stepDataPage === totalStepDataPages ? "pointer-events-none opacity-50" : "cursor-pointer" + } + /> + + + + )}
)} From addff4769ba7815c7ea6de70f289a4549395c9ec Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 24 Oct 2025 16:39:54 +0900 Subject: [PATCH 2/2] =?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 {