feat: update codex live chat workflow
This commit is contained in:
@@ -2,11 +2,12 @@ import {
|
||||
AudioOutlined,
|
||||
CodeOutlined,
|
||||
CopyOutlined,
|
||||
EyeOutlined,
|
||||
DownloadOutlined,
|
||||
FileImageOutlined,
|
||||
FileMarkdownOutlined,
|
||||
FilePdfOutlined,
|
||||
FileTextOutlined,
|
||||
FullscreenOutlined,
|
||||
LinkOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -128,6 +129,81 @@ async function copyAttachmentValue(attachment: EvidenceAttachmentItem) {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
function resolveAttachmentDownloadFileName(attachment: EvidenceAttachmentItem) {
|
||||
if (typeof attachment.title === 'string' && attachment.title.trim()) {
|
||||
return attachment.title.trim();
|
||||
}
|
||||
|
||||
if (attachment.linkUrl) {
|
||||
try {
|
||||
const resolvedUrl = new URL(
|
||||
attachment.linkUrl,
|
||||
typeof window !== 'undefined' ? window.location.origin : 'https://test.sm-home.cloud/',
|
||||
);
|
||||
const fileName = resolvedUrl.pathname.split('/').pop()?.trim();
|
||||
|
||||
if (fileName) {
|
||||
return fileName;
|
||||
}
|
||||
} catch {
|
||||
return `${attachment.key}.txt`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${attachment.key}.txt`;
|
||||
}
|
||||
|
||||
function resolveAttachmentMimeType(attachment: EvidenceAttachmentItem) {
|
||||
switch (attachment.kind) {
|
||||
case 'markdown':
|
||||
return 'text/markdown;charset=utf-8';
|
||||
case 'json':
|
||||
return 'application/json;charset=utf-8';
|
||||
case 'code':
|
||||
case 'text':
|
||||
case 'empty':
|
||||
return 'text/plain;charset=utf-8';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAttachmentValue(attachment: EvidenceAttachmentItem) {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error('download-unavailable');
|
||||
}
|
||||
|
||||
const fileName = resolveAttachmentDownloadFileName(attachment);
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (attachment.linkUrl) {
|
||||
link.href = attachment.linkUrl;
|
||||
|
||||
if (attachment.kind === 'preview') {
|
||||
link.target = '_blank';
|
||||
link.rel = 'noreferrer';
|
||||
} else {
|
||||
link.download = fileName;
|
||||
}
|
||||
} else {
|
||||
const blob = new Blob([attachment.value], {
|
||||
type: resolveAttachmentMimeType(attachment),
|
||||
});
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
link.href = objectUrl;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function resolvePreviewerType(kind: EvidenceAttachmentKind) {
|
||||
switch (kind) {
|
||||
case 'markdown':
|
||||
@@ -260,6 +336,14 @@ export function EvidenceAttachmentStrip({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload(attachment: EvidenceAttachmentItem) {
|
||||
try {
|
||||
downloadAttachmentValue(attachment);
|
||||
} catch {
|
||||
message.error('다운로드에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
if (attachments.length === 0) {
|
||||
return (
|
||||
<div className={['evidence-attachment-strip', className].filter(Boolean).join(' ')}>
|
||||
@@ -308,29 +392,27 @@ export function EvidenceAttachmentStrip({
|
||||
void handleCopy(attachment);
|
||||
}}
|
||||
/>
|
||||
{attachment.linkUrl ? (
|
||||
{attachment.linkUrl || attachment.value ? (
|
||||
<Button
|
||||
type="link"
|
||||
type="text"
|
||||
size="small"
|
||||
href={attachment.linkUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ paddingInline: 0 }}
|
||||
icon={<LinkOutlined />}
|
||||
>
|
||||
링크
|
||||
</Button>
|
||||
aria-label="다운로드"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
void handleDownload(attachment);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{onPreview ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
type="text"
|
||||
aria-label="최대화"
|
||||
icon={<FullscreenOutlined />}
|
||||
onClick={() => {
|
||||
void onPreview(attachment);
|
||||
}}
|
||||
>
|
||||
미리보기
|
||||
</Button>
|
||||
/>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { App, Card, Flex, Modal, Space, Switch, Typography } from 'antd';
|
||||
import {
|
||||
CloseOutlined,
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { App, Button, Card, Flex, Modal, Space, Switch, Typography } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import type { SampleMeta } from '../../../widgets/core';
|
||||
import {
|
||||
@@ -96,6 +103,7 @@ export function Sample() {
|
||||
const { message } = App.useApp();
|
||||
const [compact, setCompact] = useState(false);
|
||||
const [selectedAttachment, setSelectedAttachment] = useState<EvidenceAttachmentItem | null>(null);
|
||||
const [isPreviewExpanded, setIsPreviewExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16}>
|
||||
@@ -130,13 +138,51 @@ export function Sample() {
|
||||
open={Boolean(selectedAttachment)}
|
||||
title={selectedAttachment?.title ?? 'Attachment Preview'}
|
||||
footer={null}
|
||||
width={1080}
|
||||
width={isPreviewExpanded ? 'calc(100vw - 32px)' : 1080}
|
||||
onCancel={() => {
|
||||
setSelectedAttachment(null);
|
||||
setIsPreviewExpanded(false);
|
||||
}}
|
||||
>
|
||||
{selectedAttachment ? (
|
||||
<EvidenceAttachmentPreviewBody attachment={selectedAttachment} />
|
||||
<Flex vertical gap={12}>
|
||||
<Flex justify="flex-end" gap={8}>
|
||||
<Button
|
||||
aria-label="복사"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(selectedAttachment.copyValue ?? selectedAttachment.value);
|
||||
message.success(`${String(selectedAttachment.title)} 복사`);
|
||||
}}
|
||||
/>
|
||||
{selectedAttachment.linkUrl ? (
|
||||
<Button
|
||||
aria-label="다운로드"
|
||||
icon={<DownloadOutlined />}
|
||||
href={selectedAttachment.linkUrl}
|
||||
target={selectedAttachment.kind === 'preview' ? '_blank' : undefined}
|
||||
rel={selectedAttachment.kind === 'preview' ? 'noreferrer' : undefined}
|
||||
download={selectedAttachment.kind === 'preview' ? undefined : true}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
aria-label={isPreviewExpanded ? '최대화 해제' : '최대화'}
|
||||
icon={isPreviewExpanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
onClick={() => {
|
||||
setIsPreviewExpanded((previous) => !previous);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
aria-label="닫기"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => {
|
||||
setSelectedAttachment(null);
|
||||
setIsPreviewExpanded(false);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<EvidenceAttachmentPreviewBody attachment={selectedAttachment} />
|
||||
</Flex>
|
||||
) : null}
|
||||
</Modal>
|
||||
</Flex>
|
||||
|
||||
@@ -30,6 +30,7 @@ type CodexDiffBlockProps = {
|
||||
summary?: string;
|
||||
emptyDescription?: string;
|
||||
showToolbar?: boolean;
|
||||
expandAll?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -170,30 +171,42 @@ export function CodexDiffBlock({
|
||||
summary,
|
||||
emptyDescription = '표시할 diff가 없습니다.',
|
||||
showToolbar = true,
|
||||
expandAll = false,
|
||||
className,
|
||||
}: CodexDiffBlockProps) {
|
||||
const diffSections = useMemo(() => parseCodexDiffSections(diffText), [diffText]);
|
||||
const [expandedDiffPaths, setExpandedDiffPaths] = useState<string[]>(() => diffSections.slice(0, 1).map((section) => section.path));
|
||||
const [expandedDiffPath, setExpandedDiffPath] = useState<string | null>(() =>
|
||||
expandAll ? diffSections[0]?.path ?? null : diffSections[0]?.path ?? null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedDiffPaths((currentPaths) => {
|
||||
const availablePaths = new Set(diffSections.map((section) => section.path));
|
||||
const nextPaths = currentPaths.filter((path) => availablePaths.has(path));
|
||||
if (expandAll) {
|
||||
setExpandedDiffPath(diffSections[0]?.path ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextPaths.length > 0) {
|
||||
return nextPaths;
|
||||
setExpandedDiffPath((currentPath) => {
|
||||
if (currentPath && diffSections.some((section) => section.path === currentPath)) {
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
return diffSections[0] ? [diffSections[0].path] : [];
|
||||
return diffSections[0]?.path ?? null;
|
||||
});
|
||||
}, [diffSections]);
|
||||
}, [diffSections, expandAll]);
|
||||
|
||||
if (!diffSections.length) {
|
||||
return <Empty description={emptyDescription} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className ?? 'codex-diff-previewer__diff-list'}>
|
||||
<div
|
||||
className={[
|
||||
className ?? 'codex-diff-previewer__diff-list',
|
||||
expandAll ? 'codex-diff-previewer__diff-list--expand-all' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className="codex-diff-previewer__diff-toolbar">
|
||||
<Text type="secondary">
|
||||
{summary ?? `파일 ${diffSections.length}개 기준으로 diff를 분리해 표시합니다.`}
|
||||
@@ -203,15 +216,15 @@ export function CodexDiffBlock({
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setExpandedDiffPaths(diffSections.map((section) => section.path));
|
||||
setExpandedDiffPath(diffSections[0]?.path ?? null);
|
||||
}}
|
||||
>
|
||||
전체 펼치기
|
||||
첫 diff 펼치기
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setExpandedDiffPaths([]);
|
||||
setExpandedDiffPath(null);
|
||||
}}
|
||||
>
|
||||
전체 접기
|
||||
@@ -221,7 +234,7 @@ export function CodexDiffBlock({
|
||||
</div>
|
||||
|
||||
{diffSections.map((section) => {
|
||||
const isExpanded = expandedDiffPaths.includes(section.path);
|
||||
const isExpanded = expandedDiffPath === section.path;
|
||||
const displayPath = section.previousPath ? `${section.previousPath} -> ${section.path}` : section.path;
|
||||
|
||||
return (
|
||||
@@ -230,11 +243,7 @@ export function CodexDiffBlock({
|
||||
type="button"
|
||||
className="codex-diff-previewer__diff-toggle"
|
||||
onClick={() => {
|
||||
setExpandedDiffPaths((currentPaths) =>
|
||||
currentPaths.includes(section.path)
|
||||
? currentPaths.filter((path) => path !== section.path)
|
||||
: [...currentPaths, section.path],
|
||||
);
|
||||
setExpandedDiffPath((currentPath) => (currentPath === section.path ? null : section.path));
|
||||
}}
|
||||
>
|
||||
<Space align="center" size={10} className="codex-diff-previewer__diff-toggle-main">
|
||||
|
||||
@@ -58,9 +58,10 @@
|
||||
|
||||
.codex-diff-previewer__diff-section--expanded {
|
||||
position: fixed;
|
||||
inset: 16px;
|
||||
inset: 0;
|
||||
z-index: 1250;
|
||||
border-radius: 20px;
|
||||
border-radius: 0;
|
||||
border-inline: 0;
|
||||
background: #0f172a;
|
||||
box-shadow:
|
||||
0 28px 72px rgba(15, 23, 42, 0.38),
|
||||
@@ -95,7 +96,7 @@
|
||||
}
|
||||
|
||||
.codex-diff-previewer__diff-section--expanded .codex-diff-previewer__diff-body {
|
||||
height: calc(100vh - 68px);
|
||||
height: calc(100vh - 60px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
DownOutlined,
|
||||
FileImageOutlined,
|
||||
FileTextOutlined,
|
||||
@@ -92,7 +93,7 @@ export function CodexDiffPreviewer({
|
||||
}: CodexDiffPreviewerProps) {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [activeMode, setActiveMode] = useState<'source' | 'diff'>(files.length > 0 ? 'source' : 'diff');
|
||||
const [expandedSourcePaths, setExpandedSourcePaths] = useState<string[]>(() => files.slice(0, 1).map((file) => file.path));
|
||||
const [expandedSourcePath, setExpandedSourcePath] = useState<string | null>(() => files[0]?.path ?? null);
|
||||
const [expandedPreviewPath, setExpandedPreviewPath] = useState<string | null>(null);
|
||||
const statusCount = useMemo(() => buildStatusCount(files), [files]);
|
||||
const canShowSource = files.length > 0;
|
||||
@@ -129,6 +130,24 @@ export function CodexDiffPreviewer({
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(path: string, content: string) {
|
||||
if (typeof document === 'undefined') {
|
||||
messageApi.error('다운로드를 사용할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = path.split('/').pop() || 'preview.txt';
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = objectUrl;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
|
||||
function handleFullscreen(path: string) {
|
||||
setExpandedPreviewPath((currentPath) => (currentPath === path ? null : path));
|
||||
}
|
||||
@@ -141,15 +160,12 @@ export function CodexDiffPreviewer({
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedSourcePaths((currentPaths) => {
|
||||
const availablePaths = new Set(files.map((file) => file.path));
|
||||
const nextPaths = currentPaths.filter((path) => availablePaths.has(path));
|
||||
|
||||
if (nextPaths.length > 0) {
|
||||
return nextPaths;
|
||||
setExpandedSourcePath((currentPath) => {
|
||||
if (currentPath && files.some((file) => file.path === currentPath)) {
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
return files[0] ? [files[0].path] : [];
|
||||
return files[0]?.path ?? null;
|
||||
});
|
||||
}, [canShowSource, diffText, files]);
|
||||
|
||||
@@ -231,15 +247,15 @@ export function CodexDiffPreviewer({
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setExpandedSourcePaths(files.map((file) => file.path));
|
||||
setExpandedSourcePath(files[0]?.path ?? null);
|
||||
}}
|
||||
>
|
||||
전체 펼치기
|
||||
첫 문서 펼치기
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setExpandedSourcePaths([]);
|
||||
setExpandedSourcePath(null);
|
||||
}}
|
||||
>
|
||||
전체 접기
|
||||
@@ -248,7 +264,7 @@ export function CodexDiffPreviewer({
|
||||
</div>
|
||||
|
||||
{files.map((file) => {
|
||||
const isExpanded = expandedSourcePaths.includes(file.path);
|
||||
const isExpanded = expandedSourcePath === file.path;
|
||||
const isPreviewExpanded = expandedPreviewPath === file.path;
|
||||
const displayPath = file.previousPath ? `${file.previousPath} -> ${file.path}` : file.path;
|
||||
|
||||
@@ -266,11 +282,7 @@ export function CodexDiffPreviewer({
|
||||
type="button"
|
||||
className="codex-diff-previewer__diff-toggle"
|
||||
onClick={() => {
|
||||
setExpandedSourcePaths((currentPaths) =>
|
||||
currentPaths.includes(file.path)
|
||||
? currentPaths.filter((path) => path !== file.path)
|
||||
: [...currentPaths, file.path],
|
||||
);
|
||||
setExpandedSourcePath((currentPath) => (currentPath === file.path ? null : file.path));
|
||||
}}
|
||||
>
|
||||
<Space align="center" size={10} className="codex-diff-previewer__diff-toggle-main">
|
||||
@@ -296,6 +308,16 @@ export function CodexDiffPreviewer({
|
||||
void handleCopy(file.content);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
aria-label="다운로드"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDownload(file.path, file.content);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
@@ -316,6 +338,7 @@ export function CodexDiffPreviewer({
|
||||
value={file.content}
|
||||
language={file.language}
|
||||
imageAlt={file.path.split('/').pop() ?? file.path}
|
||||
downloadFileName={file.path.split('/').pop() ?? file.path}
|
||||
height={isPreviewExpanded ? 'calc(100vh - 120px)' : height}
|
||||
copyable={false}
|
||||
maximizable={false}
|
||||
|
||||
@@ -93,6 +93,15 @@
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.previewer-ui__action-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.previewer-ui--headerless {
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
@@ -105,17 +114,18 @@
|
||||
|
||||
.previewer-ui--expanded {
|
||||
position: fixed;
|
||||
inset: 16px;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
height: auto;
|
||||
border-radius: 20px;
|
||||
border-radius: 0;
|
||||
border-inline: 0;
|
||||
box-shadow:
|
||||
0 24px 64px rgba(15, 23, 42, 0.28),
|
||||
0 12px 28px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.previewer-ui--expanded .previewer-ui__body {
|
||||
height: calc(100vh - 32px - 53px) !important;
|
||||
height: calc(100vh - 53px) !important;
|
||||
}
|
||||
|
||||
.previewer-ui__language-select {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CopyOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Select, message } from 'antd';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -136,6 +136,22 @@ async function copyText(text: string) {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
function downloadBlob(content: BlobPart, fileName: string, mimeType = 'text/plain;charset=utf-8') {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error('다운로드를 사용할 수 없습니다.');
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = objectUrl;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
|
||||
function resolveCopyValue({ type, value }: Pick<PreviewerUIProps, 'type' | 'value'>) {
|
||||
switch (type) {
|
||||
case 'json':
|
||||
@@ -147,6 +163,45 @@ function resolveCopyValue({ type, value }: Pick<PreviewerUIProps, 'type' | 'valu
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDownloadValue({
|
||||
type,
|
||||
value,
|
||||
downloadValue,
|
||||
}: Pick<PreviewerUIProps, 'type' | 'value' | 'downloadValue'>) {
|
||||
if (typeof downloadValue === 'string') {
|
||||
return downloadValue;
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
return String(value ?? '');
|
||||
}
|
||||
|
||||
return resolveCopyValue({ type, value });
|
||||
}
|
||||
|
||||
function resolveDownloadFileName({
|
||||
type,
|
||||
language,
|
||||
downloadFileName,
|
||||
}: Pick<PreviewerUIProps, 'type' | 'language' | 'downloadFileName'>) {
|
||||
if (downloadFileName?.trim()) {
|
||||
return downloadFileName.trim();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'json':
|
||||
return 'preview.json';
|
||||
case 'markdown':
|
||||
return 'preview.md';
|
||||
case 'code':
|
||||
return `preview.${language || 'txt'}`;
|
||||
case 'image':
|
||||
return 'preview';
|
||||
default:
|
||||
return 'preview.txt';
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent({
|
||||
type,
|
||||
value,
|
||||
@@ -212,6 +267,10 @@ export function PreviewerUI({
|
||||
value,
|
||||
copyValue,
|
||||
copyable = true,
|
||||
downloadable = true,
|
||||
downloadValue,
|
||||
downloadUrl,
|
||||
downloadFileName,
|
||||
maximizable = true,
|
||||
language = 'text',
|
||||
format = 'auto',
|
||||
@@ -226,8 +285,11 @@ export function PreviewerUI({
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const hasLanguageSelector = type === 'code' && languageOptions && languageOptions.length > 0;
|
||||
const resolvedCopyValue = copyValue ?? resolveCopyValue({ type, value });
|
||||
const resolvedDownloadValue = resolveDownloadValue({ type, value, downloadValue });
|
||||
const resolvedDownloadFileName = resolveDownloadFileName({ type, language, downloadFileName });
|
||||
const canCopy = copyable && resolvedCopyValue.trim().length > 0;
|
||||
const shouldShowActions = hasLanguageSelector || canCopy || maximizable || Boolean(toolbar);
|
||||
const canDownload = downloadable && (Boolean(downloadUrl) || resolvedDownloadValue.trim().length > 0);
|
||||
const shouldShowActions = hasLanguageSelector || canCopy || canDownload || maximizable || Boolean(toolbar);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded || typeof document === 'undefined') {
|
||||
@@ -271,6 +333,37 @@ export function PreviewerUI({
|
||||
setIsExpanded((previous) => !previous);
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
if (!canDownload) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (downloadUrl) {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error('다운로드를 사용할 수 없습니다.');
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = resolvedDownloadFileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
const mimeType =
|
||||
type === 'json'
|
||||
? 'application/json;charset=utf-8'
|
||||
: type === 'markdown'
|
||||
? 'text/markdown;charset=utf-8'
|
||||
: 'text/plain;charset=utf-8';
|
||||
downloadBlob(resolvedDownloadValue, resolvedDownloadFileName, mimeType);
|
||||
}
|
||||
} catch {
|
||||
messageApi.error('다운로드에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
const actionContent = (
|
||||
<>
|
||||
{hasLanguageSelector ? (
|
||||
@@ -286,15 +379,27 @@ export function PreviewerUI({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
className="previewer-ui__action-button"
|
||||
aria-label="복사"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => void handleCopy()}
|
||||
/>
|
||||
) : null}
|
||||
{canDownload ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
className="previewer-ui__action-button"
|
||||
aria-label="다운로드"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
) : null}
|
||||
{maximizable ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
className="previewer-ui__action-button"
|
||||
aria-label={isExpanded ? '최대화 해제' : '최대화'}
|
||||
icon={isExpanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
onClick={() => void toggleFullscreen()}
|
||||
|
||||
@@ -16,6 +16,10 @@ export type PreviewerUIProps = {
|
||||
value?: unknown;
|
||||
copyValue?: string;
|
||||
copyable?: boolean;
|
||||
downloadable?: boolean;
|
||||
downloadValue?: string;
|
||||
downloadUrl?: string;
|
||||
downloadFileName?: string;
|
||||
maximizable?: boolean;
|
||||
language?: string;
|
||||
format?: PreviewerFormat;
|
||||
|
||||
Reference in New Issue
Block a user