chore: test deploy snapshot

This commit is contained in:
2026-05-27 10:43:01 +09:00
parent c1d0f4c1db
commit 4c4b3c8d2c
78 changed files with 10392 additions and 2301 deletions

View File

@@ -1,6 +1,6 @@
import { DeleteOutlined, LinkOutlined, PlusOutlined, QrcodeOutlined, ReloadOutlined, SaveOutlined, StopOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { Alert, App, Button, Card, Checkbox, Empty, Flex, Form, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tabs, Tag, Typography } from 'antd';
import { useEffect, useMemo, useState, type Key, type MouseEvent as ReactMouseEvent } from 'react';
import { Alert, App, Button, Card, Checkbox, Drawer, Empty, Flex, Form, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tabs, Tag, Typography } from 'antd';
import { useEffect, useLayoutEffect, useMemo, useState, type Key, type MouseEvent as ReactMouseEvent } from 'react';
import { getReadyPlayAppEntries } from '../../views/play/apps/apps/appsRegistry';
import { copyTextToClipboard } from '../../utils/clipboard';
import { openExternalLinkInNewWindow } from './mainChatPanel/linkNavigation';
@@ -22,6 +22,7 @@ import {
type SharedResourceTokenRecord,
type SharedResourceType,
} from './sharedResourceTokenAccess';
import { createInstallManifestObjectUrl, swapInstallDocumentMetadata } from './pwa/installManifest';
import './SharedResourceManagementPage.css';
const { Paragraph, Text, Title } = Typography;
@@ -45,6 +46,7 @@ const RESOURCE_TYPE_OPTIONS: Array<{ value: SharedResourceType; label: string }>
const MANAGEMENT_APP_OPTIONS = [
{ value: 'chat-live', label: 'Codex Live', description: '채팅방 조회와 응답 흐름 진입', category: '관리' },
{ value: 'chat-room-settings', label: '채팅방 설정', description: 'Codex Live 채팅방 Context 설정 편집 접근', category: '관리' },
{ value: 'text-memo-widget', label: '메모', description: '공유채팅 Apps에서 메모 컴포넌트 실행', category: '관리' },
{ value: 'token-setting', label: '토큰관리 설정', description: '토큰 설정 목록과 상세 편집 접근', category: '관리' },
{ value: 'shared-resource', label: '공유 리소스 관리', description: '공유 링크와 권한, 활동 이력 관리', category: '관리' },
{ value: 'plan-board', label: '자동화 현황', description: '작업 현황과 요청 보드 접근', category: '관리' },
@@ -83,6 +85,12 @@ type SharedResourceManagementSharedAccess = {
managedResourceTokenId?: string | null;
};
type ConversationDrawerState = {
tokenId: string;
tokenName: string;
url: string;
};
function isChatShareToken<T extends Pick<SharedResourceTokenRecord, 'resourceType'>>(
item: T | null | undefined,
): item is T & { resourceType: 'chat-share' } {
@@ -105,6 +113,14 @@ type SharedResourceTokenFormValue = {
usageLimit: number;
};
const SHARED_RESOURCE_INSTALL_THEME_COLOR = '#0f766e';
const SHARED_RESOURCE_INSTALL_BACKGROUND_COLOR = '#f3fbf9';
const SHARED_RESOURCE_INSTALL_TITLE = '공유 리소스 관리';
function buildSharedResourceInstallTitle(isSharedManageMode: boolean) {
return isSharedManageMode ? '공유 리소스 관리 링크' : SHARED_RESOURCE_INSTALL_TITLE;
}
const EMPTY_FORM_VALUE: SharedResourceTokenFormValue = {
name: '',
description: '',
@@ -417,6 +433,12 @@ function buildChatConversationUrl(item: Pick<SharedResourceTokenRecord, 'resourc
return null;
}
const shareUrl = buildAbsoluteShareUrl(item.sharePath);
if (shareUrl !== '-') {
return shareUrl;
}
const sessionId = resolveChatShareSessionId(item.sharePath || item.resourcePath);
if (!sessionId) {
@@ -470,9 +492,11 @@ function SharedResourceQrPanel({
export function SharedResourceManagementPage({
sharedPreview = null,
sharedAccess = null,
disableInstallMetadata = false,
}: {
sharedPreview?: SharedResourceManagementSharedPreview | null;
sharedAccess?: SharedResourceManagementSharedAccess | null;
disableInstallMetadata?: boolean;
}) {
const { message } = App.useApp();
const { hasAccess } = useTokenAccess();
@@ -491,9 +515,43 @@ export function SharedResourceManagementPage({
const [activeDetailTab, setActiveDetailTab] = useState<'basic' | 'settings' | 'history'>('basic');
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]);
const [qrPreviewTokenId, setQrPreviewTokenId] = useState<string | null>(null);
const [conversationDrawer, setConversationDrawer] = useState<ConversationDrawerState | null>(null);
const [conversationDrawerKey, setConversationDrawerKey] = useState(0);
const [form] = Form.useForm<SharedResourceTokenFormValue>();
const [modalApi, modalContextHolder] = Modal.useModal();
useLayoutEffect(() => {
if (disableInstallMetadata || typeof window === 'undefined') {
return undefined;
}
const startPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
const installTitle = buildSharedResourceInstallTitle(isSharedManageMode);
const manifestObjectUrl = createInstallManifestObjectUrl({
startPath,
scope: window.location.pathname,
name: installTitle,
shortName: '공유 리소스',
description: isSharedManageMode
? '공유 리소스 관리 링크를 홈 화면 앱으로 바로 엽니다.'
: '공유 리소스 관리 화면을 홈 화면 앱으로 바로 엽니다.',
themeColor: SHARED_RESOURCE_INSTALL_THEME_COLOR,
backgroundColor: SHARED_RESOURCE_INSTALL_BACKGROUND_COLOR,
});
const restoreManifest = swapInstallDocumentMetadata({
manifestHref: manifestObjectUrl,
title: installTitle,
themeColor: SHARED_RESOURCE_INSTALL_THEME_COLOR,
});
return () => {
restoreManifest();
if (manifestObjectUrl) {
window.URL.revokeObjectURL(manifestObjectUrl);
}
};
}, [disableInstallMetadata, isSharedManageMode]);
useEffect(() => {
const intervalId = window.setInterval(() => {
setNowMs(Date.now());
@@ -632,17 +690,26 @@ export function SharedResourceManagementPage({
const openConversationWindow = (url: string, event?: ReactMouseEvent<HTMLElement>) => {
openExternalLinkInNewWindow(url, {
event,
allowSameTabFallback: false,
onUnsupportedStandalone: (fallbackUrl) => {
void copyTextToClipboard(fallbackUrl)
.then(() => {
message.info('현재 모바일 PWA에서는 preview 앱 열기도 막혀 URL 복사했습니다. 브라우저에서 붙여 열어 주세요.');
message.info('현재 창은 유지하고 공유채팅 URL 복사했습니다. 브라우저나 새 PWA 창에서 붙여 열어 주세요.');
})
.catch(() => {
message.info('현재 모바일 PWA에서는 새 창과 preview 앱 열기가 막힐 수 있습니다. QR 코드나 URL 복사로 이어서 열어 주세요.');
message.info('현재 창은 유지했습니다. 새 창 열기가 막히면 QR 코드나 URL 복사로 이어서 열어 주세요.');
});
},
});
};
const openConversationDrawer = (tokenId: string, tokenName: string, url: string) => {
setConversationDrawer({
tokenId,
tokenName,
url,
});
setConversationDrawerKey((current) => current + 1);
};
const listColumns = useMemo(
() => [
@@ -740,7 +807,7 @@ export function SharedResourceManagementPage({
return;
}
openConversationWindow(conversationUrl, event);
openConversationDrawer(item.id, item.name, conversationUrl);
}}
>
@@ -796,6 +863,21 @@ export function SharedResourceManagementPage({
key: 'detail',
render: (value: string | null) => sanitizeActivityDetail(value) ?? '-',
},
{
title: '접속 정보',
key: 'ip',
render: (_value: unknown, item: SharedResourceTokenActivityRecord) => {
const lines = [
item.externalIp ? `외부 ${item.externalIp}` : null,
item.clientIp ? `서버 ${item.clientIp}` : null,
item.forwardedFor ? `XFF ${item.forwardedFor}` : null,
item.clientId ? `client ${item.clientId}` : null,
].filter(Boolean);
return lines.length > 0 ? (
<Typography.Text style={{ whiteSpace: 'pre-line' }}>{lines.join('\n')}</Typography.Text>
) : '-';
},
},
{
title: '사용량',
dataIndex: 'usageDelta',
@@ -1148,6 +1230,38 @@ export function SharedResourceManagementPage({
<Empty description="QR 코드를 표시할 공유 URL이 없습니다." />
)}
</Modal>
<Drawer
open={Boolean(conversationDrawer)}
title={conversationDrawer ? `${conversationDrawer.tokenName} 채팅` : '공유 채팅'}
placement="right"
width="100vw"
rootClassName="shared-resource-management-page__conversation-drawer"
onClose={() => {
setConversationDrawer(null);
}}
extra={
conversationDrawer ? (
<Space size={8}>
<Button onClick={() => setConversationDrawerKey((current) => current + 1)}></Button>
<Button onClick={(event) => openConversationWindow(conversationDrawer.url, event)}> </Button>
</Space>
) : null
}
styles={{
body: {
padding: 0,
},
}}
>
{conversationDrawer ? (
<iframe
key={`${conversationDrawer.tokenId}-${conversationDrawerKey}`}
title={`${conversationDrawer.tokenName} 공유 채팅`}
src={conversationDrawer.url}
className="shared-resource-management-page__conversation-frame"
/>
) : null}
</Drawer>
{detailMode === 'list' ? (
<Card
title="공유 리소스 관리"
@@ -1422,9 +1536,9 @@ export function SharedResourceManagementPage({
type="link"
icon={<LinkOutlined />}
style={{ paddingInline: 0, marginTop: 4 }}
onClick={(event) => openConversationWindow(detailConversationUrl, event)}
onClick={() => openConversationDrawer(detailData.token.id, detailData.token.name, detailConversationUrl)}
>
</Button>
) : null}
</div>