feat: V2 WebView 컴포넌트 + SSO 연동 구현
- V2WebView 컴포넌트: iframe 기반 외부 웹 임베딩 - SSO 연동: 현재 로그인 JWT를 sso_token 파라미터로 자동 전달 - /api/system/raw-token: 범용 JWT 토큰 조회 API - V2WebViewConfigPanel: URL, SSO, sandbox 등 설정 UI + 개발자 가이드 Made-with: Cursor
This commit is contained in:
@@ -119,6 +119,7 @@ import "./v2-approval-step/ApprovalStepRenderer"; // 결재 단계 시각화
|
||||
import "./v2-status-count/StatusCountRenderer"; // 상태별 카운트 카드
|
||||
import "./v2-process-work-standard/ProcessWorkStandardRenderer"; // 공정 작업기준
|
||||
import "./v2-item-routing/ItemRoutingRenderer"; // 품목별 라우팅
|
||||
import "./v2-web-view/V2WebViewRenderer"; // 외부 웹페이지 임베딩 (SSO 지원)
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { ComponentRendererProps } from "../../types";
|
||||
import { V2WebViewConfig } from "./types";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
|
||||
export interface V2WebViewComponentProps extends ComponentRendererProps {}
|
||||
|
||||
export const V2WebViewComponent: React.FC<V2WebViewComponentProps> = ({
|
||||
component,
|
||||
isDesignMode = false,
|
||||
isSelected = false,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const config = (component.componentConfig || {}) as V2WebViewConfig;
|
||||
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const baseUrl = config.url ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!baseUrl) {
|
||||
setIframeSrc(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.useSSO) {
|
||||
setIframeSrc(baseUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const paramName = "sso_token";
|
||||
|
||||
fetch("/api/system/raw-token")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
if (data.token) {
|
||||
const separator = baseUrl.includes("?") ? "&" : "?";
|
||||
setIframeSrc(`${baseUrl}${separator}${encodeURIComponent(paramName)}=${encodeURIComponent(data.token)}`);
|
||||
} else {
|
||||
setError(data.error ?? "토큰을 가져올 수 없습니다");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setError("토큰 조회 실패");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [baseUrl, config.useSSO]);
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
left: `${component.style?.positionX || 0}px`,
|
||||
top: `${component.style?.positionY || 0}px`,
|
||||
width: `${component.style?.width || 400}px`,
|
||||
height: `${component.style?.height || 300}px`,
|
||||
zIndex: component.style?.positionZ || 1,
|
||||
cursor: isDesignMode ? "pointer" : "default",
|
||||
border: isSelected ? "2px solid #3b82f6" : config.showBorder ? "1px solid #e0e0e0" : "none",
|
||||
borderRadius: config.borderRadius || "8px",
|
||||
overflow: "hidden",
|
||||
background: "#fafafa",
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isDesignMode) {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
}
|
||||
};
|
||||
|
||||
const domProps = filterDOMProps(props);
|
||||
|
||||
// 디자인 모드: URL 미리보기 표시
|
||||
if (isDesignMode) {
|
||||
return (
|
||||
<div style={containerStyle} className="v2-web-view-component" onClick={handleClick} {...domProps}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "8px",
|
||||
color: "#666",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</svg>
|
||||
<span style={{ fontWeight: 500 }}>웹 뷰</span>
|
||||
{baseUrl ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "#999",
|
||||
maxWidth: "90%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{baseUrl}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: "11px", color: "#bbb" }}>URL을 설정하세요</span>
|
||||
)}
|
||||
{config.useSSO && <span style={{ fontSize: "10px", color: "#4caf50" }}>SSO: ?sso_token=JWT</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 런타임 모드
|
||||
return (
|
||||
<div style={containerStyle} className="v2-web-view-component" {...domProps}>
|
||||
{loading && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#999",
|
||||
}}
|
||||
>
|
||||
{config.loadingText || "로딩 중..."}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#f44336",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && iframeSrc && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
style={{ width: "100%", height: "100%", border: "none" }}
|
||||
sandbox={config.sandbox ? "allow-scripts allow-same-origin allow-forms allow-popups" : undefined}
|
||||
allowFullScreen={config.allowFullscreen}
|
||||
title="Web View"
|
||||
/>
|
||||
)}
|
||||
{!loading && !error && !iframeSrc && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#bbb",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
URL이 설정되지 않았습니다
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const V2WebViewWrapper: React.FC<V2WebViewComponentProps> = (props) => {
|
||||
return <V2WebViewComponent {...props} />;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { V2WebViewDefinition } from "./index";
|
||||
import { V2WebViewComponent } from "./V2WebViewComponent";
|
||||
|
||||
export class V2WebViewRenderer extends AutoRegisteringComponentRenderer {
|
||||
static componentDefinition = V2WebViewDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
return <V2WebViewComponent {...this.props} renderer={this} />;
|
||||
}
|
||||
}
|
||||
|
||||
V2WebViewRenderer.registerSelf();
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import { V2WebViewWrapper } from "./V2WebViewComponent";
|
||||
import { V2WebViewConfigPanel } from "@/components/v2/config-panels/V2WebViewConfigPanel";
|
||||
import type { V2WebViewConfig } from "./types";
|
||||
|
||||
export const V2WebViewDefinition = createComponentDefinition({
|
||||
id: "v2-web-view",
|
||||
name: "V2 웹 뷰",
|
||||
nameEng: "V2 WebView Component",
|
||||
description: "외부 웹페이지를 iframe으로 임베드하여 표시하는 컴포넌트 (SSO 지원)",
|
||||
category: ComponentCategory.DISPLAY,
|
||||
webType: "custom",
|
||||
component: V2WebViewWrapper,
|
||||
defaultConfig: {
|
||||
url: "",
|
||||
useSSO: false,
|
||||
sandbox: true,
|
||||
allowFullscreen: false,
|
||||
showBorder: true,
|
||||
loadingText: "로딩 중...",
|
||||
} as V2WebViewConfig,
|
||||
defaultSize: { width: 600, height: 400 },
|
||||
configPanel: V2WebViewConfigPanel,
|
||||
icon: "Globe",
|
||||
tags: ["v2", "웹", "뷰", "iframe", "임베드", "외부", "SSO", "fleet"],
|
||||
version: "1.0.0",
|
||||
author: "개발팀",
|
||||
});
|
||||
|
||||
export type { V2WebViewConfig } from "./types";
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ComponentConfig } from "@/types/component";
|
||||
|
||||
export interface V2WebViewConfig extends ComponentConfig {
|
||||
url?: string;
|
||||
useSSO?: boolean;
|
||||
sandbox?: boolean;
|
||||
allowFullscreen?: boolean;
|
||||
borderRadius?: string;
|
||||
showBorder?: boolean;
|
||||
loadingText?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user