237 lines
10 KiB
TypeScript
237 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
import { Globe, Shield, Settings, ChevronDown, Info, Copy, Check } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import type { V2WebViewConfig } from "@/lib/registry/components/v2-web-view/types";
|
|
|
|
interface V2WebViewConfigPanelProps {
|
|
config: V2WebViewConfig;
|
|
onChange: (config: Partial<V2WebViewConfig>) => void;
|
|
}
|
|
|
|
const SSO_GUIDE_SNIPPET = `// URL에서 sso_token 파라미터를 읽어 JWT를 디코딩하세요
|
|
const token = url.searchParams.get("sso_token");
|
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
// payload.userId, payload.userName, payload.companyCode`;
|
|
|
|
export const V2WebViewConfigPanel: React.FC<V2WebViewConfigPanelProps> = ({ config, onChange }) => {
|
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
const [guideOpen, setGuideOpen] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopySnippet = () => {
|
|
navigator.clipboard.writeText(SSO_GUIDE_SNIPPET).then(() => {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
});
|
|
};
|
|
|
|
const updateConfig = (field: keyof V2WebViewConfig, value: any) => {
|
|
const newConfig = { ...config, [field]: value };
|
|
onChange({ [field]: value });
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(
|
|
new CustomEvent("componentConfigChanged", {
|
|
detail: { config: newConfig },
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* ─── 1단계: URL 입력 ─── */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Globe className="text-primary h-4 w-4" />
|
|
<p className="text-sm font-medium">웹페이지 URL</p>
|
|
</div>
|
|
<Input
|
|
value={config.url || ""}
|
|
onChange={(e) => updateConfig("url", e.target.value)}
|
|
placeholder="https://example.com"
|
|
className="h-8 text-sm"
|
|
/>
|
|
<p className="text-muted-foreground text-[11px]">임베드할 외부 웹페이지 주소를 입력하세요</p>
|
|
</div>
|
|
|
|
{/* ─── 2단계: SSO 연동 ─── */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between py-1">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="text-muted-foreground h-4 w-4" />
|
|
<div>
|
|
<p className="text-sm">SSO 연동</p>
|
|
<p className="text-muted-foreground text-[11px]">현재 로그인 토큰을 URL에 자동 전달해요</p>
|
|
</div>
|
|
</div>
|
|
<Switch checked={config.useSSO || false} onCheckedChange={(checked) => updateConfig("useSSO", checked)} />
|
|
</div>
|
|
|
|
{config.useSSO && (
|
|
<Collapsible open={guideOpen} onOpenChange={setGuideOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-left transition-colors hover:bg-blue-100 dark:border-blue-900 dark:bg-blue-950 dark:hover:bg-blue-900"
|
|
>
|
|
<Info className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
|
<span className="text-[11px] font-medium text-blue-700 dark:text-blue-300">연동 개발자 가이드</span>
|
|
<ChevronDown
|
|
className={cn(
|
|
"ml-auto h-3.5 w-3.5 text-blue-400 transition-transform duration-200",
|
|
guideOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-2.5 rounded-b-lg border border-t-0 border-blue-200 bg-blue-50/50 px-3 py-3 dark:border-blue-900 dark:bg-blue-950/50">
|
|
<div className="space-y-1">
|
|
<p className="text-[11px] font-medium text-blue-800 dark:text-blue-200">전달 방식</p>
|
|
<p className="text-muted-foreground text-[10px]">URL 쿼리 파라미터로 JWT가 전달됩니다.</p>
|
|
<code className="bg-muted/80 block rounded px-2 py-1 text-[10px]">?sso_token=eyJhbGciOi...</code>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-[11px] font-medium text-blue-800 dark:text-blue-200">JWT Payload 구조</p>
|
|
<div className="bg-muted/80 rounded px-2 py-1.5 text-[10px] leading-relaxed">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">userId</span>
|
|
<span>사용자 ID</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">userName</span>
|
|
<span>사용자 이름</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">companyCode</span>
|
|
<span>회사 코드</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">role</span>
|
|
<span>권한</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-[11px] font-medium text-blue-800 dark:text-blue-200">수신측 예시 코드</p>
|
|
<button
|
|
type="button"
|
|
onClick={handleCopySnippet}
|
|
className="flex items-center gap-1 text-[10px] text-blue-500 hover:text-blue-700"
|
|
>
|
|
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
|
{copied ? "복사됨" : "복사"}
|
|
</button>
|
|
</div>
|
|
<pre className="bg-muted/80 overflow-x-auto rounded px-2 py-1.5 text-[10px] leading-relaxed whitespace-pre-wrap">
|
|
{`const token = url.searchParams
|
|
.get("sso_token");
|
|
const payload = JSON.parse(
|
|
atob(token.split(".")[1])
|
|
);
|
|
// payload.userId
|
|
// payload.companyCode`}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
</div>
|
|
|
|
{/* ─── 3단계: 표시 옵션 ─── */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">테두리 표시</p>
|
|
<p className="text-muted-foreground text-[11px]">웹 뷰 주변에 테두리를 표시해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showBorder !== false}
|
|
onCheckedChange={(checked) => updateConfig("showBorder", checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">전체 화면 허용</p>
|
|
<p className="text-muted-foreground text-[11px]">임베드된 페이지에서 전체 화면 전환이 가능해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.allowFullscreen || false}
|
|
onCheckedChange={(checked) => updateConfig("allowFullscreen", checked)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ─── 4단계: 고급 설정 (기본 접혀있음) ─── */}
|
|
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-sm font-medium">고급 설정</span>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
|
advancedOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">샌드박스 모드</p>
|
|
<p className="text-muted-foreground text-[11px]">보안을 위해 iframe 실행 환경을 제한해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.sandbox !== false}
|
|
onCheckedChange={(checked) => updateConfig("sandbox", checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">모서리 둥글기</span>
|
|
<Input
|
|
value={config.borderRadius || "8px"}
|
|
onChange={(e) => updateConfig("borderRadius", e.target.value)}
|
|
placeholder="8px"
|
|
className="h-7 w-[100px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">로딩 텍스트</span>
|
|
<Input
|
|
value={config.loadingText || ""}
|
|
onChange={(e) => updateConfig("loadingText", e.target.value)}
|
|
placeholder="로딩 중..."
|
|
className="h-7 w-[140px] text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
V2WebViewConfigPanel.displayName = "V2WebViewConfigPanel";
|
|
|
|
export default V2WebViewConfigPanel;
|