ERP-node/frontend/lib/registry/components/v2-web-view/V2WebViewComponent.tsx

196 lines
5.6 KiB
TypeScript
Raw Normal View History

"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} />;
};