196 lines
5.6 KiB
TypeScript
196 lines
5.6 KiB
TypeScript
"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} />;
|
|
};
|