chore: test deploy snapshot

This commit is contained in:
2026-05-28 22:47:45 +09:00
parent 753fd423db
commit 1e7212b862
6 changed files with 348 additions and 52 deletions

View File

@@ -5171,9 +5171,11 @@ export function MainChatPanel({
activePreview,
isPreviewLoading,
isPreviewModalOpen,
previewModalTarget,
previewContentType,
previewError,
previewText,
handlePreviewModalAfterOpenChange,
setActivePreviewId,
setActivePreviewOverride,
setIsPreviewModalOpen,
@@ -9456,16 +9458,17 @@ export function MainChatPanel({
</Space>
</Modal>
<FullscreenPreviewModal
open={isPreviewModalOpen && Boolean(activePreview)}
open={isPreviewModalOpen}
afterOpenChange={handlePreviewModalAfterOpenChange}
title={
activePreview
? `${activePreview.label} · ${activePreview.kind} · ${activePreview.source === 'context' ? '현재 화면' : '채팅 결과'}`
previewModalTarget
? `${previewModalTarget.label} · ${previewModalTarget.kind} · ${previewModalTarget.source === 'context' ? '현재 화면' : '채팅 결과'}`
: 'preview'
}
meta={null}
actions={
<>
{isActivePreviewHtml ? (
{previewModalTarget && isHtmlPreviewItem(previewModalTarget) ? (
<>
<Button
type="text"
@@ -9487,7 +9490,7 @@ export function MainChatPanel({
/>
</>
) : null}
{canSearchActivePreview ? (
{previewModalTarget && canSearchActivePreview ? (
<Button
type="text"
className="fullscreen-preview-modal__icon-button"
@@ -9526,7 +9529,7 @@ export function MainChatPanel({
}}
className={`app-chat-panel__preview-modal${isHtmlPreviewFullscreen ? ' app-chat-panel__preview-modal--html-mobile' : ''}`}
>
{activePreview ? (
{previewModalTarget ? (
<div className="app-chat-panel__preview-modal-body">
{canSearchActivePreview && isPreviewFindOpen ? (
<div className="app-chat-panel__preview-modal-findbar">
@@ -9570,7 +9573,7 @@ export function MainChatPanel({
onPointerDownCapture={handlePreviewSearchRootPointerDown}
>
<ChatPreviewBody
target={activePreview}
target={previewModalTarget}
previewText={previewText}
isPreviewLoading={isPreviewLoading}
previewError={previewError}

View File

@@ -42,6 +42,7 @@ export function useConversationViewController({
const [activePreviewId, setActivePreviewId] = useState<string | null>(null);
const [activePreviewOverride, setActivePreviewOverride] = useState<PreviewItem | null>(null);
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
const [previewModalTarget, setPreviewModalTarget] = useState<PreviewItem | null>(null);
const [previewText, setPreviewText] = useState('');
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
const [previewError, setPreviewError] = useState('');
@@ -49,6 +50,14 @@ export function useConversationViewController({
const activePreview = activePreviewOverride ?? previewItems.find((item) => item.id === activePreviewId) ?? previewItems[0] ?? null;
useEffect(() => {
if (!isPreviewModalOpen || !activePreview) {
return;
}
setPreviewModalTarget(activePreview);
}, [activePreview, isPreviewModalOpen]);
useEffect(() => {
const previousSessionId = previousSessionIdRef.current;
const hasSessionChanged = previousSessionId !== activeSessionId;
@@ -98,7 +107,7 @@ export function useConversationViewController({
}, [activePreviewId, activePreviewOverride, previewItems]);
useEffect(() => {
if (!isPreviewModalOpen || !activePreview) {
if (!previewModalTarget) {
setPreviewText('');
setPreviewError('');
setPreviewContentType('');
@@ -106,11 +115,15 @@ export function useConversationViewController({
return;
}
if (!isPreviewModalOpen) {
return;
}
if (
activePreview.kind === 'image' ||
activePreview.kind === 'video' ||
activePreview.kind === 'pdf' ||
activePreview.kind === 'file'
previewModalTarget.kind === 'image' ||
previewModalTarget.kind === 'video' ||
previewModalTarget.kind === 'pdf' ||
previewModalTarget.kind === 'file'
) {
setPreviewText('');
setPreviewError('');
@@ -124,7 +137,7 @@ export function useConversationViewController({
setPreviewError('');
setPreviewContentType('');
fetch(activePreview.url, {
fetch(previewModalTarget.url, {
cache: 'no-store',
signal: controller.signal,
})
@@ -155,7 +168,19 @@ export function useConversationViewController({
return () => {
controller.abort();
};
}, [activePreview, isPreviewModalOpen]);
}, [isPreviewModalOpen, previewModalTarget]);
const handlePreviewModalAfterOpenChange = (open: boolean) => {
if (open) {
return;
}
setPreviewModalTarget(null);
setPreviewText('');
setPreviewError('');
setPreviewContentType('');
setIsPreviewLoading(false);
};
useEffect(() => {
const previousActiveView = previousActiveViewRef.current;
@@ -216,9 +241,11 @@ export function useConversationViewController({
activePreviewId,
isPreviewLoading,
isPreviewModalOpen,
previewModalTarget,
previewContentType,
previewError,
previewText,
handlePreviewModalAfterOpenChange,
setActivePreviewId,
setActivePreviewOverride,
setIsPreviewModalOpen,

View File

@@ -1626,6 +1626,7 @@ async function requestChatApi<T>(
init?: RequestInit,
options?: {
allowUnauthenticated?: boolean;
signal?: AbortSignal;
timeoutMs?: number;
sharePin?: string | null;
},
@@ -1635,9 +1636,18 @@ async function requestChatApi<T>(
const accessToken = getRegisteredAccessToken();
const method = init?.method?.toUpperCase() ?? 'GET';
const controller = new AbortController();
const externalAbortHandler = () => controller.abort();
const timeoutMs = Number.isFinite(options?.timeoutMs) ? Math.max(1000, Number(options?.timeoutMs)) : 8000;
const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
if (options?.signal) {
if (options.signal.aborted) {
window.clearTimeout(timeoutId);
throw new DOMException('Aborted', 'AbortError');
}
options.signal.addEventListener('abort', externalAbortHandler, { once: true });
}
if (!allowUnauthenticated && !hasRegisteredAccessTokenAccess()) {
window.clearTimeout(timeoutId);
throw new Error('등록된 접근 토큰이 없어 채팅 요청을 보낼 수 없습니다.');
@@ -1681,8 +1691,14 @@ async function requestChatApi<T>(
});
} catch (error) {
window.clearTimeout(timeoutId);
if (options?.signal) {
options.signal.removeEventListener('abort', externalAbortHandler);
}
if (error instanceof DOMException && error.name === 'AbortError') {
if (options?.signal?.aborted) {
throw error;
}
throw new Error('채팅 서버 응답이 지연됩니다.');
}
@@ -1690,6 +1706,9 @@ async function requestChatApi<T>(
}
window.clearTimeout(timeoutId);
if (options?.signal) {
options.signal.removeEventListener('abort', externalAbortHandler);
}
if (!response.ok) {
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
@@ -2847,6 +2866,7 @@ export async function fetchChatShareSnapshot(
options?: {
sharePin?: string | null;
sessionId?: string | null;
signal?: AbortSignal;
view?: 'full' | 'initial';
},
) {
@@ -2882,6 +2902,7 @@ export async function fetchChatShareSnapshot(
undefined,
{
allowUnauthenticated: true,
signal: options?.signal,
sharePin: options?.sharePin,
timeoutMs: 20000,
},

View File

@@ -1,7 +1,7 @@
import { AppstoreOutlined, CheckOutlined, CloseOutlined, CommentOutlined, CopyOutlined, DeleteOutlined, DownOutlined, EditOutlined, EyeInvisibleOutlined, EyeOutlined, FileTextOutlined, FilterOutlined, FullscreenOutlined, LeftOutlined, MinusOutlined, PlusOutlined, ReloadOutlined, RightOutlined, SearchOutlined, SendOutlined, SettingOutlined, ThunderboltOutlined, UpOutlined } from '@ant-design/icons';
import { Alert, App, Button, Checkbox, Drawer, Dropdown, Input, Modal, Select, Spin, Tabs, Tag, Typography, type MenuProps } from 'antd';
import type { TextAreaRef } from 'antd/es/input/TextArea';
import { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ClipboardEvent, type CSSProperties, type FocusEvent, type KeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react';
import { Suspense, lazy, startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ClipboardEvent, type CSSProperties, type FocusEvent, type KeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useParams } from 'react-router-dom';
import { FullscreenPreviewModal } from '../../../components/previewer';
@@ -110,6 +110,8 @@ const SHARE_LAST_ROOM_STORAGE_KEY = 'codex-live-share-last-room-by-token';
const SHARE_ROOM_SNAPSHOT_SESSION_INDEX_STORAGE_KEY = 'codex-live-share-room-snapshot-index:v1';
const SHARE_ROOM_SNAPSHOT_SESSION_STORAGE_KEY_PREFIX = 'codex-live-share-room-snapshot:v1';
const SHARE_ROOM_SNAPSHOT_SESSION_CACHE_LIMIT = 6;
const SHARE_ROOM_SWITCH_CACHE_REQUEST_LIMIT = 12;
const SHARE_ROOM_SWITCH_CACHE_MESSAGE_LIMIT = 32;
const SHARE_IMMEDIATE_SEND_TOGGLE_HOLD_MS = 1000;
const SHARE_EDGE_NAVIGATION_HOTZONE_PX = 28;
const SHARE_APPS_EDGE_MIDDLE_BAND_RATIO = 0.2;
@@ -140,6 +142,28 @@ type ShareMessageRenderPayload = ShareRenderedMessage & {
previewItems: PreviewItem[];
};
type ShareRoomSwitchPerfStep = {
name: string;
at: number;
detailLevel?: ChatShareSnapshot['detailLevel'];
requestCount?: number;
messageCount?: number;
activityLogCount?: number;
hasCache?: boolean;
};
type ShareRoomSwitchPerfRun = {
id: string;
fromSessionId: string;
toSessionId: string;
startedAt: number;
steps: ShareRoomSwitchPerfStep[];
};
type ShareWindowWithPerf = Window & {
__chatShareRoomSwitchPerfRuns?: ShareRoomSwitchPerfRun[];
};
type ShareExpandMode = 'latest' | 'pending' | 'all';
type PendingSharePromptSelection = PromptDraftSelection & {
status: 'draft' | 'submitted';
@@ -593,7 +617,11 @@ function readStoredShareRoomSnapshot(token: string, sessionId: string) {
}
const cachedSessionId = resolveShareSnapshotCacheSessionId(snapshot);
return cachedSessionId === normalizedSessionId ? snapshot : null;
if (cachedSessionId !== normalizedSessionId) {
return null;
}
return buildShareRoomSwitchCacheSnapshot(snapshot);
} catch {
return null;
}
@@ -643,9 +671,10 @@ function clearStoredShareRoomSnapshotCache(token: string) {
function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot | null | undefined) {
const normalizedToken = token.trim();
const normalizedSessionId = resolveShareSnapshotCacheSessionId(snapshot);
const storedSnapshot = buildShareRoomSwitchCacheSnapshot(snapshot ?? null);
const normalizedSessionId = resolveShareSnapshotCacheSessionId(storedSnapshot);
if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId || !snapshot) {
if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId || !storedSnapshot) {
return;
}
@@ -656,7 +685,7 @@ function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot
buildShareRoomSnapshotSessionStorageKey(normalizedToken, normalizedSessionId),
JSON.stringify({
savedAt: nextSavedAt,
snapshot,
snapshot: storedSnapshot,
}),
);
const nextIndex = readStoredShareRoomSnapshotSessionIndex();
@@ -681,6 +710,91 @@ function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot
}
}
function buildShareRoomSwitchCacheSnapshot(snapshot: ChatShareSnapshot | null | undefined) {
if (!snapshot) {
return null;
}
if (snapshot.detailLevel === 'initial') {
return snapshot;
}
const trimmedRequests = snapshot.requests.slice(-SHARE_ROOM_SWITCH_CACHE_REQUEST_LIMIT);
const requestIdSet = new Set(trimmedRequests.map((request) => request.requestId));
const trimmedMessages = snapshot.messages
.filter((message) => {
const requestId = message.requestId?.trim() ?? '';
return !requestId || requestIdSet.has(requestId);
})
.slice(-SHARE_ROOM_SWITCH_CACHE_MESSAGE_LIMIT);
const oldestLoadedMessageId =
trimmedMessages.length > 0
? trimmedMessages[0]?.id ?? snapshot.oldestLoadedMessageId
: snapshot.oldestLoadedMessageId;
return {
...snapshot,
detailLevel: 'initial',
requests: trimmedRequests,
messages: trimmedMessages,
activityLogs: [],
oldestLoadedMessageId,
hasOlderMessages: true,
} satisfies ChatShareSnapshot;
}
function startShareRoomSwitchPerfRun(fromSessionId: string, toSessionId: string) {
if (typeof window === 'undefined') {
return '';
}
const perfWindow = window as ShareWindowWithPerf;
const nextRun: ShareRoomSwitchPerfRun = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
fromSessionId: fromSessionId.trim(),
toSessionId: toSessionId.trim(),
startedAt: performance.now(),
steps: [],
};
const nextRuns = [...(perfWindow.__chatShareRoomSwitchPerfRuns ?? []).slice(-9), nextRun];
perfWindow.__chatShareRoomSwitchPerfRuns = nextRuns;
return nextRun.id;
}
function recordShareRoomSwitchPerfStep(
runId: string,
name: string,
snapshot?: ChatShareSnapshot | null,
extra?: Pick<ShareRoomSwitchPerfStep, 'hasCache'>,
) {
if (typeof window === 'undefined' || !runId) {
return;
}
const perfWindow = window as ShareWindowWithPerf;
const runs = perfWindow.__chatShareRoomSwitchPerfRuns;
if (!runs?.length) {
return;
}
const targetRun = runs.find((run) => run.id === runId);
if (!targetRun) {
return;
}
targetRun.steps.push({
name,
at: performance.now(),
detailLevel: snapshot?.detailLevel,
requestCount: snapshot?.requests.length,
messageCount: snapshot?.messages.length,
activityLogCount: snapshot?.activityLogs.length,
hasCache: extra?.hasCache,
});
}
function readShareRoomSessionIdFromLocation() {
if (typeof window === 'undefined') {
return '';
@@ -4074,6 +4188,7 @@ export function ChatSharePage() {
typeof window === 'undefined' || !normalizedToken
? null
: readStoredShareRoomSnapshot(normalizedToken, initialRequestedRoomSessionId);
const initialHasCachedSnapshot = initialCachedSnapshot != null;
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const pageRef = useRef<HTMLDivElement | null>(null);
const requestAnchorRefs = useRef(new Map<string, HTMLElement>());
@@ -4083,6 +4198,11 @@ export function ChatSharePage() {
const deferredSnapshotRef = useRef<ChatShareSnapshot | null>(null);
const liveRefreshTimerRef = useRef<number | null>(null);
const snapshotRefreshPromiseRef = useRef<Promise<boolean> | null>(null);
const snapshotRefreshAbortControllerRef = useRef<AbortController | null>(null);
const snapshotRefreshInFlightRoomSessionIdRef = useRef('');
const snapshotRefreshInFlightViewRef = useRef<'initial' | 'full' | null>(null);
const roomSwitchPerfRunIdRef = useRef('');
const pendingSnapshotPerfStageRef = useRef<{ runId: string; stage: 'cache' | 'initial' | 'full' } | null>(null);
const pendingSilentRefreshRef = useRef(false);
const scrollSyncFrameRef = useRef<number | null>(null);
const scrollIdleTimerRef = useRef<number | null>(null);
@@ -4232,6 +4352,21 @@ export function ChatSharePage() {
const isRoomSwitchingRef = useRef(isRoomSwitching);
isRoomSwitchingRef.current = isRoomSwitching;
useLayoutEffect(() => {
const pendingStage = pendingSnapshotPerfStageRef.current;
if (!pendingStage || !snapshot) {
return;
}
pendingSnapshotPerfStageRef.current = null;
recordShareRoomSwitchPerfStep(pendingStage.runId, `${pendingStage.stage}-snapshot-commit`, snapshot);
window.requestAnimationFrame(() => {
recordShareRoomSwitchPerfStep(pendingStage.runId, `${pendingStage.stage}-first-frame`, snapshot);
});
}, [snapshot]);
const shareTokenSetting = snapshot?.share.tokenSetting ?? null;
const shareAllowedAppIdSet = useMemo(
() => buildShareAllowedAppIdSet(shareTokenSetting?.allowedAppIds, snapshot?.share.permissions),
@@ -4858,35 +4993,65 @@ export function ChatSharePage() {
void refreshShareRuntime();
}, [isRoomSettingsOpen, refreshShareRuntime, roomSettingsTabKey]);
const refreshSnapshot = useCallback(async (options?: { initialLoad?: boolean; silent?: boolean; sharePin?: string | null }) => {
const refreshSnapshot = useCallback(async (options?: {
initialLoad?: boolean;
silent?: boolean;
sharePin?: string | null;
view?: 'initial' | 'full';
preserveVisibleSnapshot?: boolean;
}) => {
if (!normalizedToken) {
return false;
}
const initialLoad = options?.initialLoad === true;
const silent = options?.silent === true;
const requestView = options?.view ?? (initialLoad ? 'initial' : 'full');
const preserveVisibleSnapshot = options?.preserveVisibleSnapshot === true;
const currentPerfRunId = roomSwitchPerfRunIdRef.current;
const requestedSessionId = requestedRoomSessionIdRef.current.trim();
if (snapshotRefreshPromiseRef.current) {
if (!initialLoad) {
pendingSilentRefreshRef.current = pendingSilentRefreshRef.current || silent;
const inFlightRoomSessionId = snapshotRefreshInFlightRoomSessionIdRef.current;
const inFlightView = snapshotRefreshInFlightViewRef.current;
const shouldReplaceInFlightRequest =
requestView === 'initial'
|| requestedSessionId !== inFlightRoomSessionId
|| inFlightView !== requestView;
if (shouldReplaceInFlightRequest) {
snapshotRefreshAbortControllerRef.current?.abort();
} else {
if (!initialLoad) {
pendingSilentRefreshRef.current = pendingSilentRefreshRef.current || silent;
}
return snapshotRefreshPromiseRef.current;
}
return snapshotRefreshPromiseRef.current;
}
const abortController = new AbortController();
snapshotRefreshAbortControllerRef.current = abortController;
snapshotRefreshInFlightRoomSessionIdRef.current = requestedSessionId;
snapshotRefreshInFlightViewRef.current = requestView;
if (options?.initialLoad) {
setIsLoading(true);
} else {
if (!(preserveVisibleSnapshot && hasSnapshotRef.current)) {
setIsLoading(true);
}
} else if (!silent) {
setIsRefreshing(true);
}
const refreshTask = (async () => {
const refreshTaskPromise = (async () => {
try {
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-fetch-start`);
const nextSnapshot = await fetchChatShareSnapshot(normalizedToken, {
sharePin: options?.sharePin,
sessionId: requestedRoomSessionIdRef.current || undefined,
view: initialLoad ? 'initial' : 'full',
sessionId: requestedSessionId || undefined,
signal: abortController.signal,
view: requestView,
});
const requestedSessionId = requestedRoomSessionIdRef.current.trim();
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-fetch-end`, nextSnapshot);
const matchedRequestedRoom = doesShareSnapshotMatchRequestedRoom(nextSnapshot, requestedSessionId);
const shouldApplyImmediately =
@@ -4899,12 +5064,19 @@ export function ChatSharePage() {
if (!shouldApplyImmediately) {
deferredSnapshotRef.current = nextSnapshot;
} else {
setSnapshot(nextSnapshot);
pendingSnapshotPerfStageRef.current = currentPerfRunId
? { runId: currentPerfRunId, stage: requestView }
: null;
if (!initialLoad && requestView === 'full' && hasSnapshotRef.current) {
startTransition(() => {
setSnapshot(nextSnapshot);
});
} else {
setSnapshot(nextSnapshot);
}
deferredSnapshotRef.current = null;
}
if (nextSnapshot.detailLevel !== 'initial') {
writeStoredShareRoomSnapshot(normalizedToken, nextSnapshot);
}
writeStoredShareRoomSnapshot(normalizedToken, nextSnapshot);
if (nextSnapshot.share.hasAccessPin) {
const resolvedPin = normalizeAccessPinInput(options?.sharePin ?? '');
const persistedPin = resolvedPin || getStoredChatShareAccessPin(normalizedToken);
@@ -4919,6 +5091,7 @@ export function ChatSharePage() {
}
if (matchedRequestedRoom) {
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-matched-requested-room`, nextSnapshot);
setIsRoomSwitching(false);
} else if (requestedSessionId) {
pendingSilentRefreshRef.current = true;
@@ -4930,6 +5103,10 @@ export function ChatSharePage() {
return true;
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
return false;
}
if (error instanceof ChatApiError && error.status === 401 && (error.code === 'share_pin_required' || error.code === 'share_pin_invalid')) {
setStoredChatShareAccessPin(normalizedToken, null);
clearStoredShareRoomSnapshotCache(normalizedToken);
@@ -4950,7 +5127,16 @@ export function ChatSharePage() {
setIsRoomSwitching(false);
return false;
} finally {
snapshotRefreshPromiseRef.current = null;
if (snapshotRefreshAbortControllerRef.current === abortController) {
snapshotRefreshAbortControllerRef.current = null;
}
if (snapshotRefreshPromiseRef.current === refreshTaskPromise) {
snapshotRefreshPromiseRef.current = null;
}
if (snapshotRefreshInFlightRoomSessionIdRef.current === requestedSessionId) {
snapshotRefreshInFlightRoomSessionIdRef.current = '';
snapshotRefreshInFlightViewRef.current = null;
}
setIsLoading(false);
setIsRefreshing(false);
@@ -4963,9 +5149,40 @@ export function ChatSharePage() {
}
})();
snapshotRefreshPromiseRef.current = refreshTask;
return refreshTask;
snapshotRefreshPromiseRef.current = refreshTaskPromise;
return refreshTaskPromise;
}, [normalizedToken]);
const ensureLatestShareRoomDetailLoaded = useCallback(
async (requestId?: string | null) => {
const normalizedRequestId = requestId?.trim() ?? '';
if (!normalizedToken || !snapshot?.conversation.sessionId) {
return;
}
if (
normalizedRequestId
&& snapshot.activityLogs.some((item) => item.requestId.trim() === normalizedRequestId)
) {
return;
}
setIsLoadingFullSnapshot(true);
try {
const detailPage = await fetchChatShareConversationDetail(normalizedToken, {
sessionId: snapshot.conversation.sessionId,
limit: Math.max(SHARE_ROOM_SWITCH_CACHE_REQUEST_LIMIT, snapshot.requests.length || 0),
sharePin: getStoredChatShareAccessPin(normalizedToken) || undefined,
});
setSnapshot((current) => (current ? mergeChatShareSnapshotHistory(current, detailPage) : current));
} finally {
setIsLoadingFullSnapshot(false);
}
},
[normalizedToken, snapshot],
);
const handleLoadOlderShareHistory = useCallback(async () => {
if (!normalizedToken || !snapshot?.conversation.sessionId || !snapshot.hasOlderMessages || !snapshot.oldestLoadedMessageId) {
return;
@@ -5676,15 +5893,27 @@ export function ChatSharePage() {
event.preventDefault();
}, []);
const openProcessInspector = useCallback((requestId: string) => {
if (!requestId.trim()) {
const openProcessInspector = useCallback(async (requestId: string) => {
const normalizedRequestId = requestId.trim();
if (!normalizedRequestId) {
return;
}
setActiveProcessInspectorRequestId(requestId);
const hasActivityLog = snapshot?.activityLogs.some((item) => item.requestId.trim() === normalizedRequestId) ?? false;
if (!hasActivityLog) {
try {
await ensureLatestShareRoomDetailLoaded(normalizedRequestId);
} catch (error) {
message.error(error instanceof Error ? error.message : '상세 활동 로그를 불러오지 못했습니다.');
}
}
setActiveProcessInspectorRequestId(normalizedRequestId);
setProcessInspectorMode('default');
setProcessInspectorExpandedSection('checklist');
}, []);
}, [ensureLatestShareRoomDetailLoaded, message, snapshot?.activityLogs]);
const closeProcessInspector = useCallback(() => {
setActiveProcessInspectorRequestId('');
@@ -6083,9 +6312,13 @@ export function ChatSharePage() {
return undefined;
}
void refreshSnapshot({ initialLoad: true });
void refreshSnapshot({
initialLoad: true,
view: 'initial',
preserveVisibleSnapshot: initialHasCachedSnapshot,
});
return undefined;
}, [normalizedToken, refreshSnapshot]);
}, [initialHasCachedSnapshot, normalizedToken, refreshSnapshot]);
useEffect(() => {
if (typeof window === 'undefined') {
return;
@@ -6094,7 +6327,6 @@ export function ChatSharePage() {
const urlRoomSessionId = readShareRoomSessionIdFromLocation();
const restoredRoomSessionId = urlRoomSessionId || readStoredShareLastRoomSessionId(normalizedToken);
const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, restoredRoomSessionId);
requestedRoomSessionIdRef.current = restoredRoomSessionId;
deferredSnapshotRef.current = null;
setSnapshot(cachedSnapshot);
@@ -6116,7 +6348,7 @@ export function ChatSharePage() {
if (cachedSnapshot) {
setSnapshot(cachedSnapshot);
}
setIsRoomSwitching(Boolean(nextRoomSessionId));
setIsRoomSwitching(Boolean(nextRoomSessionId) && !cachedSnapshot);
setRequestedRoomSessionId(nextRoomSessionId);
setIsShareRoomListVisible(false);
};
@@ -6137,8 +6369,15 @@ export function ChatSharePage() {
return;
}
void refreshSnapshot({ silent: true });
}, [normalizedToken, refreshSnapshot, requestedRoomSessionId]);
const requestedSessionId = requestedRoomSessionId.trim();
const hasMatchingSnapshot = doesShareSnapshotMatchRequestedRoom(snapshot, requestedSessionId);
if (hasMatchingSnapshot) {
return;
}
void refreshSnapshot({ silent: true, view: 'initial' });
}, [normalizedToken, refreshSnapshot, requestedRoomSessionId, snapshot]);
useEffect(() => {
if (!normalizedToken || !snapshot || snapshot.detailLevel === 'initial') {
return;
@@ -6688,9 +6927,13 @@ export function ChatSharePage() {
}
const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, normalizedSessionId);
const perfRunId = startShareRoomSwitchPerfRun(selectedShareRoomSessionId, normalizedSessionId);
deferredSnapshotRef.current = null;
roomSwitchPerfRunIdRef.current = perfRunId;
recordShareRoomSwitchPerfStep(perfRunId, 'room-select', undefined, { hasCache: Boolean(cachedSnapshot) });
if (cachedSnapshot) {
pendingSnapshotPerfStageRef.current = { runId: perfRunId, stage: 'cache' };
setSnapshot(cachedSnapshot);
}
setIsRoomSwitching(!cachedSnapshot);
@@ -8366,7 +8609,11 @@ export function ChatSharePage() {
const normalizedRoomSessionId = restoreSnapshot.roomSessionId.trim();
if (normalizedRoomSessionId && normalizedRoomSessionId !== selectedShareRoomSessionId) {
setIsRoomSwitching(true);
const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, normalizedRoomSessionId);
if (cachedSnapshot) {
setSnapshot(cachedSnapshot);
}
setIsRoomSwitching(!cachedSnapshot);
requestedRoomSessionIdRef.current = normalizedRoomSessionId;
writeStoredShareLastRoomSessionId(normalizedToken, normalizedRoomSessionId);
writeShareRoomSessionIdToLocation(normalizedRoomSessionId, 'replace');

View File

@@ -5,6 +5,7 @@ import './FullscreenPreviewModal.css';
type FullscreenPreviewModalProps = {
open: boolean;
afterOpenChange?: (open: boolean) => void;
title?: ReactNode;
meta?: ReactNode;
actions?: ReactNode;
@@ -25,6 +26,7 @@ type FullscreenPreviewModalProps = {
export function FullscreenPreviewModal({
open,
afterOpenChange,
title,
meta,
actions,
@@ -45,6 +47,7 @@ export function FullscreenPreviewModal({
return (
<Modal
open={open}
afterOpenChange={afterOpenChange}
footer={null}
title={null}
width="100vw"

View File

@@ -2056,11 +2056,6 @@ export function BaseballTicketBayPlayAppView({ onBack, launchContext = 'direct',
<span>{alerts.length}</span>
</div>
</div>
{isViewingAllClients || isSharedTokenScope ? (
<div className="baseball-ticket-bay-app__scope-note">
{isSharedTokenScope ? '현재 공유 리소스 별명 기준으로 목록을 표시합니다.' : '등록 토큰으로 전체 기기 목록을 보고 있습니다. 리소스 별명으로 확인하고 수정과 삭제도 바로 할 수 있습니다.'}
</div>
) : null}
<div className="baseball-ticket-bay-app__items">
{alerts.length ? (
alerts.map((item) => {