import { CheckCircleOutlined, CopyOutlined, LinkOutlined, PlusOutlined } from '@ant-design/icons'; import { Alert, App, Button, Card, Checkbox, Empty, Input, Result, Space, Steps, Tag, Typography } from 'antd'; import { useMemo, useState } from 'react'; import { useChatTypeRegistry } from './chatTypeAccess'; import { useTokenSettingRegistry, type TokenSettingRecord } from './tokenSettingAccess'; import { useTokenAccess } from './tokenAccess'; import { createManagedChatShareRoom, type ManagedChatShareRoom } from './mainChatPanel'; import { openExternalLinkInNewWindow } from './mainChatPanel/linkNavigation'; import { resolveChatPathForSession } from './chatSessionRouting'; import { copyTextToClipboard } from '../../utils/clipboard'; import './SharedChatManagementPage.css'; const { Paragraph, Text, Title } = Typography; type SharedChatDraft = { tokenSettingId: string; chatTypeId: string; name: string; requestBadgeLabel: string; seedMessage: string; allowManageAccess: boolean; }; const DEFAULT_DRAFT: SharedChatDraft = { tokenSettingId: '', chatTypeId: '', name: '', requestBadgeLabel: '', seedMessage: '이 공유채팅방에서 원하는 작업 내용을 남겨 주세요. 필요한 파일이나 참고 문맥이 있으면 함께 적어 주세요.', allowManageAccess: false, }; function hasAllowedApp(setting: Pick, appId: string) { return setting.allowedAppIds.some((item) => item.trim().toLowerCase() === appId); } function resolveAbsoluteUrl(pathname: string) { if (typeof window === 'undefined') { return pathname; } try { return new URL(pathname, window.location.origin).toString(); } catch { return pathname; } } export function SharedChatManagementPage() { const { message } = App.useApp(); const { hasAccess } = useTokenAccess(); const { tokenSettings, isLoading: isTokenSettingsLoading, errorMessage: tokenSettingsErrorMessage } = useTokenSettingRegistry(); const { chatTypes, isLoading: isChatTypesLoading, errorMessage: chatTypesErrorMessage } = useChatTypeRegistry(); const [currentStep, setCurrentStep] = useState(0); const [draft, setDraft] = useState(DEFAULT_DRAFT); const [createErrorMessage, setCreateErrorMessage] = useState(''); const [isCreating, setIsCreating] = useState(false); const [createdRoom, setCreatedRoom] = useState<(ManagedChatShareRoom & { shareUrl: string; conversationUrl: string }) | null>(null); const availableTokenSettings = useMemo( () => tokenSettings.filter((item) => item.enabled && hasAllowedApp(item, 'chat-live')), [tokenSettings], ); const availableChatTypes = useMemo(() => chatTypes.filter((item) => item.enabled), [chatTypes]); const selectedTokenSetting = useMemo( () => availableTokenSettings.find((item) => item.id === draft.tokenSettingId) ?? null, [availableTokenSettings, draft.tokenSettingId], ); const selectedChatType = useMemo( () => availableChatTypes.find((item) => item.id === draft.chatTypeId) ?? null, [availableChatTypes, draft.chatTypeId], ); const canGrantManageAccess = useMemo( () => Boolean( selectedTokenSetting && (hasAllowedApp(selectedTokenSetting, 'chat-room-settings') || hasAllowedApp(selectedTokenSetting, 'token-setting') || hasAllowedApp(selectedTokenSetting, 'shared-resource') || hasAllowedApp(selectedTokenSetting, 'server-command')), ), [selectedTokenSetting], ); const suggestedShareName = useMemo( () => (selectedChatType ? `${selectedChatType.name} 공유채팅` : ''), [selectedChatType], ); const stepItems = [ { title: '공유 토큰' }, { title: '채팅 유형' }, { title: '채팅방 정보' }, { title: 'URL 공유' }, ]; const openManagedShareWindow = (url: string) => { openExternalLinkInNewWindow(url, { onUnsupportedStandalone: (fallbackUrl) => { void copyTextToClipboard(fallbackUrl) .then(() => { message.info('현재 모바일 PWA에서는 preview 앱 열기도 막혀 URL을 복사했습니다. 브라우저에서 붙여 열어 주세요.'); }) .catch(() => { message.info('현재 모바일 PWA에서는 새 창과 preview 앱 열기가 막힐 수 있습니다. URL 복사 후 브라우저에서 열어 주세요.'); }); }, }); }; const handleMoveNext = () => { if (currentStep === 0 && !selectedTokenSetting) { message.warning('공유 토큰 설정을 먼저 선택하세요.'); return; } if (currentStep === 1 && !selectedChatType) { message.warning('채팅 유형을 먼저 선택하세요.'); return; } if (currentStep === 2) { if (!draft.name.trim()) { message.warning('공유 이름을 입력하세요.'); return; } if (!draft.seedMessage.trim()) { message.warning('공유 시작 문구를 입력하세요.'); return; } } setCurrentStep((previous) => Math.min(previous + 1, stepItems.length - 1)); }; const handleCreateRoom = async () => { if (!selectedTokenSetting || !selectedChatType) { message.warning('공유 토큰과 채팅 유형을 먼저 선택하세요.'); return; } const name = draft.name.trim(); const seedMessage = draft.seedMessage.trim(); if (!name || !seedMessage) { message.warning('공유 이름과 공유 시작 문구를 입력하세요.'); return; } setIsCreating(true); setCreateErrorMessage(''); try { const created = await createManagedChatShareRoom({ tokenSettingId: selectedTokenSetting.id, chatTypeId: selectedChatType.id, chatTypeLabel: selectedChatType.name, name, requestBadgeLabel: draft.requestBadgeLabel.trim() || null, seedMessage, allowManageAccess: draft.allowManageAccess && canGrantManageAccess, }); const shareUrl = resolveAbsoluteUrl(created.sharePath); const conversationUrl = resolveAbsoluteUrl(`${resolveChatPathForSession(created.sessionId)}?sessionId=${encodeURIComponent(created.sessionId)}`); setCreatedRoom({ ...created, shareUrl, conversationUrl, }); setCurrentStep(3); message.success('공유채팅방을 생성했습니다.'); } catch (error) { setCreateErrorMessage(error instanceof Error ? error.message : '공유채팅방 생성에 실패했습니다.'); } finally { setIsCreating(false); } }; const handleReset = () => { setDraft(DEFAULT_DRAFT); setCurrentStep(0); setCreateErrorMessage(''); setCreatedRoom(null); }; if (!hasAccess) { return ( ); } return (
} onClick={handleReset}> 새로 만들기 ) : null } >
{tokenSettingsErrorMessage ? : null} {chatTypesErrorMessage ? : null} {createErrorMessage ? : null}
{currentStep === 0 ? ( 1. 공유 토큰 설정 선택 `chat-live` 권한이 있는 토큰 설정만 표시합니다. 이 설정이 공유 URL의 만료 시간과 사용량 한도를 결정합니다. {availableTokenSettings.length > 0 ? (
{availableTokenSettings.map((item) => { const active = item.id === draft.tokenSettingId; return ( ); })}
) : ( )}
) : null} {currentStep === 1 ? ( 2. 채팅 유형 선택 공유 URL에 들어온 사용자는 이 채팅 유형 기준으로 대화를 이어갑니다. {availableChatTypes.length > 0 ? (
{availableChatTypes.map((item) => { const active = item.id === draft.chatTypeId; return ( ); })}
) : ( )}
) : null} {currentStep === 2 ? ( 3. 채팅방 정보 입력
setDraft((previous) => ({ ...previous, allowManageAccess: event.target.checked }))} > 공유 참여자에게 관리 권한 허용 {canGrantManageAccess ? '선택 시 채팅방 설정 또는 연결된 관리 화면 접근이 열립니다.' : '선택한 토큰 설정에는 노출 가능한 관리 앱 권한이 없습니다.'}
공유 토큰
{selectedTokenSetting?.name ?? '-'}
생성 이름
{draft.name.trim() || '-'}
유효 시간
{selectedTokenSetting ? selectedTokenSetting.defaultExpiresInMinutes > 0 ? `${selectedTokenSetting.defaultExpiresInMinutes}분` : '무제한' : '-'}
접근 보호
공유받은 사용자가 필요할 때 채팅방 설정에서 직접 설정
) : null} {currentStep === 3 ? ( {createdRoom ? ( } title="공유채팅방 URL이 준비되었습니다." subTitle="이 URL로 외부 사용자가 같은 채팅방 흐름에 이어서 질문할 수 있습니다." extra={ } /> ) : ( )} {createdRoom ? (
적용 권한
{createdRoom.permissions.map((permission) => ( {permission} ))}
) : null}
) : null}
{currentStep < 2 ? ( ) : currentStep === 2 ? ( ) : null}
); }