import * as AntdIcons from '@ant-design/icons';
import { AppstoreOutlined, CheckOutlined, 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, 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';
import { BaseballTicketBayPlayAppView } from '../../../views/play/apps/baseball-ticket-bay/BaseballTicketBayPlayAppView';
import { EReaderAppView } from '../../../views/play/apps/e-reader/EReaderAppView';
import {
  findReadyPlayAppEntryById,
  getReadyPlayAppEntries,
  getSupportedPlayAppEnvironments,
  isPlayAppSupportedInEnvironment,
  type PlayAppEntry,
  type PlayAppEnvironment,
} from '../../../views/play/apps/apps/appsRegistry';
import { PhotoPuzzleAppView } from '../../../views/play/apps/photo-puzzle/PhotoPuzzleAppView';
import { PhotoPrismAppView } from '../../../views/play/apps/photoprism/PhotoPrismAppView';
import { TetrisAppView } from '../../../views/play/apps/tetris/TetrisAppView';
import { TheQuestAppView } from '../../../views/play/apps/the-quest/TheQuestAppView';
import { Template1PlayAppView } from '../../../views/play/apps/template1/Template1PlayAppView';
import { SharedResourceManagementPage } from '../SharedResourceManagementPage';
import { SharedAppSettingsPage } from '../SharedAppSettingsPage';
import { TokenSettingManagementPage } from '../TokenSettingManagementPage';
import { ServerCommandPage } from '../../../features/serverCommand';
import { fetchServerCommands } from '../../../features/serverCommand/api';
import type { ServerCommandItem } from '../../../features/serverCommand/types';
import { saveAppConfigToServer, setStoredAppConfig, useAppConfig } from '../appConfig';
import { useChatTypeRegistry } from '../chatTypeAccess';
import {
  resolveChatRoomContextSettings,
  resolveChatTypeDefaultContextIds,
  upsertChatRoomContextSettings,
  useChatContextSettingsRegistry,
} from '../chatContextSettingsAccess';
import { ChatPromptCard, buildPromptTargetSignature, type PromptDraftSelection, type PromptSubmitPayload } from '../mainChatPanel/ChatPromptCard';
import { ChatPreviewBody, type ChatPreviewTarget } from '../mainChatPanel/ChatPreviewBody';
import {
  cancelChatShareRuntimeRequest,
  cancelChatShareRequest,
  ChatApiError,
  clearChatShareConversationRoom,
  completeChatShareManualBadge,
  createChatShareRoom,
  fetchChatConversationDetail,
  fetchChatShareConversationDetail,
  fetchChatConversations,
  deleteChatShareRoom,
  fetchChatShareRuntimeSnapshot,
  fetchChatShareSnapshot,
  getStoredChatShareAccessPin,
  resolveChatWebSocketUrl,
  retryChatShareRequest,
  saveChatShareRoomSettings,
  setStoredChatShareAccessPin,
  submitChatShareOriginReply,
  submitChatShareMessage,
  submitChatSharePrompt,
  uploadChatShareComposerFile,
  type ChatShareRoomSummary,
  type ChatShareSnapshot
} from '../mainChatPanel/chatUtils';
import { DEFAULT_CODEX_MODEL, CODEX_MODEL_OPTIONS, normalizeCodexModel } from '../codexModelOptions';
import { extractAttachmentPreviewUrls, extractChatMessageParts } from '../mainChatPanel/messageParts';
import { stripHiddenPreviewTags } from '../mainChatPanel/previewMarkers';
import { extractPreviewItems, type PreviewItem } from '../mainChatPanel/previewItems';
import { buildChatPath, buildPlayAppPath } from '../routes';
import type { PreviewKind } from '../mainChatPanel/previewKind';
import { normalizeChatResourceUrl } from '../mainChatPanel/chatResourceUrl';
import { forceReloadApp } from '../appUpdate';
import { resetNonAuthClientState } from '../appMaintenance';
import type {
  ChatComposerAttachment,
  ChatConversationSummary,
  ChatConversationRequest,
  ChatMessage,
  ChatMessagePart,
  ChatShareRoomLinkContext,
  ChatRuntimeJobItem,
  ChatRuntimeSnapshot,
  ChatRuntimeTerminalStatus,
  ChatServerEvent,
} from '../mainChatPanel/types';
import { isPromptResolved } from '../mainChatPanel/promptState';
import { sendClientNotification, shouldFallbackToLocalNotification, showLocalClientNotification } from '../notificationApi';
import { copyTextToClipboard } from '../../../utils/clipboard';
import { isTypingTarget } from '../mainView/utils';
import { applyViewportCssVars, scheduleViewportRecoverySync } from '../viewportCssVars';
import { isPreviewRuntime } from '../previewRuntime';
import { createInstallManifestObjectUrl, swapInstallDocumentMetadata } from '../pwa/installManifest';
import { getSavedNotificationDeviceId } from '../notificationIdentity';
import { ensureWebPushSubscriptionRegistered, syncExistingWebPushSubscriptionRegistration } from '../webPushRegistration';
import '../mainChatPanel/styles/MainChatPanel.conversation.css';
import '../mainChatPanel/styles/MainChatPanel.preview-runtime.css';
import './ChatSharePage.css';

const { Paragraph, Text, Title } = Typography;

const ShareCloseIcon = AntdIcons.CloseOutlined || AntdIcons.CloseCircleOutlined || (() => <span aria-hidden="true">×</span>);

const CHAT_API_RESOURCE_ROUTE_PREFIX = '/api/chat/resources/';
const CHAT_PUBLIC_ROUTE_PREFIX = '/.codex_chat/';
const CHAT_PUBLIC_DOT_CODEX_PREFIX = '/public/.codex_chat/';
const RESOURCE_MANAGER_PREVIEW_ROUTE_PREFIX = '/api/resource-manager/preview/';
const RESOURCE_MANAGER_ROOT_PREFIX = 'resource/';
const SHARE_COMPOSER_VIEWPORT_COMPACT_THRESHOLD_PX = 24;
const SHARE_ACCESS_PIN_MAX_LENGTH = 4;
const SHARE_PROCESSING_CLOCK_INTERVAL_MS = 60 * 1000;
const SHARE_TOKEN_USAGE_CLOCK_INTERVAL_MS = 1000;
const SHARE_EXPIRY_CLOCK_INTERVAL_MS = 60 * 1000;
const SHARE_IMMEDIATE_SEND_PINNED_STORAGE_KEY = 'codex-live-share-immediate-send-pinned-by-token';
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 = 38;
const SHARE_EDGE_GESTURE_MIN_HORIZONTAL_PX = 16;
const SHARE_EDGE_GESTURE_OPEN_APPS_PX = 100;
const SHARE_EDGE_APPS_GESTURE_HOTZONE_PX = 110;
const SHARE_MINIMIZED_ACTION_TAP_TOLERANCE_PX = 8;
const SHARE_HISTORY_PAGE_SIZE = 40;
const SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS = [
  { value: 'always', label: '매번 묻기', minutes: 0 },
  { value: '5', label: '5분 유지', minutes: 5 },
  { value: '30', label: '30분 유지', minutes: 30 },
  { value: '60', label: '1시간 유지', minutes: 60 },
  { value: '180', label: '3시간 유지', minutes: 180 },
  { value: '1440', label: '24시간 유지', minutes: 1440 },
] as const;

type SharePreviewFetchError = Error & { status?: number };

type ShareRequestCardMode = 'question-only' | 'answer-only' | 'full';

type ShareRenderedMessage = {
  visibleText: string;
  promptParts: Extract<ChatMessagePart, { type: 'prompt' }>[];
  diffBlocks: string[];
};

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';
};
type ShareProgramRestoreSnapshot = {
  roomSessionId: string;
  latestRequestId: string;
  expandMode: ShareExpandMode;
  scrollTop: number;
};
type ShareProgramTarget = {
  key: string;
  label: string;
  url: string;
  kind: PreviewKind;
  meta?: string;
  appId?: string;
  restoreSnapshot?: ShareProgramRestoreSnapshot | null;
};
type ShareMinimizedProgramItem = {
  target: ShareProgramTarget;
  position: {
    x: number;
    y: number;
  };
};
type ShareMinimizedProgramAction = 'restore' | 'close';
type ShareMinimizedProgramActionTracking = {
  key: string;
  pointerId: number;
  startX: number;
  startY: number;
  action: ShareMinimizedProgramAction;
  captureTarget: HTMLElement;
  cancelled: boolean;
};
type ShareAppEnvironment = PlayAppEnvironment;
type ShareSearchResult = {
  key: string;
  title: string;
  description: string;
  category: 'request' | 'response' | 'resource' | 'activity';
  icon?: ReactNode;
  usageBadge?: string | null;
  requestId?: string;
  resource?: ShareProgramTarget | null;
  appEntry?: PlayAppEntry | null;
  scrollTarget?: {
    type: 'request' | 'activity' | 'response' | 'prompt';
    value: string;
  };
};
type ShareSearchIndexedResult = ShareSearchResult & {
  searchText: string;
};
type ShareEdgeGestureTracking =
  | {
    startX: number;
    startY: number;
    startTime: number;
    direction: 'back';
  }
  | {
    startX: number;
    startY: number;
    startTime: number;
    direction: 'apps';
    opened: boolean;
  };
type ShareSearchPanelMode = 'all' | 'apps';
type ShareWorkServerVersionStatus = 'latest' | 'unknown' | 'update-available' | 'build-required';
type ClientNotificationPermissionState = 'unsupported' | 'default' | 'granted' | 'denied';
type ShareNotificationStatusTone = 'success' | 'warning' | 'default';
type ShareNotificationClientStatus = {
  roomLabel: string;
  appLabel: string;
  permissionLabel: string;
  registrationLabel: string;
  summaryLabel: string;
  tone: ShareNotificationStatusTone;
};
type ShareProcessInspectorMode = 'default' | 'fullscreen' | 'minimized';
type ShareProcessInspectorExpandedSection = 'summary' | 'checklist' | 'narratives' | 'log' | null;
type ShareProcessChecklistStep = {
  key: string;
  label: string;
  status: 'pending' | 'in_progress' | 'completed';
  note: string;
};
type ShareProcessInspectorPayload = {
  requestId: string;
  statusTag: { color: string; label: string };
  summary: string;
  elapsedLabel: string;
  startedAtLabel: string;
  updatedAtLabel: string;
  activityLines: string[];
  latestActivityLine: string;
  checklist: ShareProcessChecklistStep[];
  narratives: string[];
};
type ShareRoomPendingCounts = {
  processingCount: number;
  unansweredCount: number;
};

type ShareRoomSourceGroup = {
  key: string;
  title: string;
  requestPreview: string;
  chatTypeLabel: string;
  sourceSessionId: string | null;
  sourceRequestId: string | null;
  linkContext: ChatShareRoomLinkContext | null;
  rooms: ChatShareRoomSummary[];
};

const LazyTextMemoWidget = lazy(async () => {
  const module = await import('../../../widgets/text-memo-widget');
  return { default: module.TextMemoWidget };
});

function normalizeAccessPinInput(value: string) {
  return value.replace(/\D+/gu, '').slice(0, SHARE_ACCESS_PIN_MAX_LENGTH);
}

function resolveShareConversationRequestSortKey(request: ChatConversationRequest) {
  const userMessageId = typeof request.userMessageId === 'number' && Number.isFinite(request.userMessageId)
    ? request.userMessageId
    : null;

  if (userMessageId != null) {
    return userMessageId;
  }

  const responseMessageId = typeof request.responseMessageId === 'number' && Number.isFinite(request.responseMessageId)
    ? request.responseMessageId
    : null;

  if (responseMessageId != null) {
    return responseMessageId;
  }

  const createdAt = Date.parse(request.createdAt?.trim() ?? '');
  return Number.isFinite(createdAt) ? createdAt : null;
}

function compareShareConversationRequests(left: ChatConversationRequest, right: ChatConversationRequest) {
  const leftSortKey = resolveShareConversationRequestSortKey(left);
  const rightSortKey = resolveShareConversationRequestSortKey(right);

  if (leftSortKey != null && rightSortKey != null && leftSortKey !== rightSortKey) {
    return leftSortKey - rightSortKey;
  }

  const leftCreatedAt = Date.parse(left.createdAt?.trim() ?? '');
  const rightCreatedAt = Date.parse(right.createdAt?.trim() ?? '');
  if (Number.isFinite(leftCreatedAt) && Number.isFinite(rightCreatedAt) && leftCreatedAt !== rightCreatedAt) {
    return leftCreatedAt - rightCreatedAt;
  }

  return left.requestId.localeCompare(right.requestId, 'ko');
}

function isSnapshotDeferrableFocusTarget(target: EventTarget | null) {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  const tagName = target.tagName.toLowerCase();
  if (tagName === 'textarea') {
    return true;
  }

  if (tagName === 'input') {
    const inputType = target.getAttribute('type')?.toLowerCase() ?? 'text';
    return !['button', 'checkbox', 'file', 'hidden', 'radio', 'range', 'reset', 'submit'].includes(inputType);
  }

  return target.isContentEditable;
}

function resolveAccessPinPromptTtlMinutes(value?: number | null) {
  if (value == null) {
    return 0;
  }

  return Number.isFinite(value) ? Math.max(0, Number(value)) : 0;
}

function resolveAccessPinPromptTtlOptionValue(value?: number | null) {
  const normalizedValue = resolveAccessPinPromptTtlMinutes(value);
  return SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS.find((item) => item.minutes === normalizedValue)?.value ?? 'always';
}

function resolveAccessPinPromptTtlLabel(value?: number | null) {
  const option = SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS.find((item) => item.minutes === resolveAccessPinPromptTtlMinutes(value));
  return option?.label ?? '매번 묻기';
}

function parseAccessPinPromptTtlOptionValue(value: string) {
  const matchedOption = SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS.find((item) => item.value === value);
  return matchedOption?.minutes ?? null;
}

function canDeleteShareRoom(room: ChatShareRoomSummary, rooms: ChatShareRoomSummary[]) {
  if (room.isDefault) {
    return false;
  }

  return rooms.length > 1;
}

function readStoredShareImmediateSendPinnedByToken() {
  if (typeof window === 'undefined') {
    return {} as Record<string, boolean>;
  }

  try {
    const raw = window.localStorage.getItem(SHARE_IMMEDIATE_SEND_PINNED_STORAGE_KEY);

    if (!raw) {
      return {} as Record<string, boolean>;
    }

    const parsed = JSON.parse(raw) as Record<string, unknown>;

    return Object.entries(parsed).reduce<Record<string, boolean>>((result, [token, isPinned]) => {
      const normalizedToken = String(token ?? '').trim();

      if (normalizedToken && typeof isPinned === 'boolean') {
        result[normalizedToken] = isPinned;
      }

      return result;
    }, {});
  } catch {
    return {} as Record<string, boolean>;
  }
}

function writeStoredShareImmediateSendPinnedByToken(nextMap: Record<string, boolean>) {
  if (typeof window === 'undefined') {
    return;
  }

  const normalizedMap = Object.entries(nextMap).reduce<Record<string, boolean>>((result, [token, isPinned]) => {
    const normalizedToken = String(token ?? '').trim();

    if (normalizedToken && typeof isPinned === 'boolean') {
      result[normalizedToken] = isPinned;
    }

    return result;
  }, {});

  window.localStorage.setItem(SHARE_IMMEDIATE_SEND_PINNED_STORAGE_KEY, JSON.stringify(normalizedMap));
}

function readStoredShareLastRoomMapFromStorage(storage: Storage | null | undefined) {
  if (!storage) {
    return {} as Record<string, string>;
  }

  try {
    const raw = storage.getItem(SHARE_LAST_ROOM_STORAGE_KEY);

    if (!raw) {
      return {} as Record<string, string>;
    }

    const parsed = JSON.parse(raw) as Record<string, unknown>;

    return Object.entries(parsed).reduce<Record<string, string>>((result, [token, sessionId]) => {
      const normalizedToken = String(token ?? '').trim();
      const normalizedSessionId = String(sessionId ?? '').trim();

      if (normalizedToken && normalizedSessionId) {
        result[normalizedToken] = normalizedSessionId;
      }

      return result;
    }, {});
  } catch {
    return {} as Record<string, string>;
  }
}

function readStoredShareLastRoomByToken() {
  if (typeof window === 'undefined') {
    return {} as Record<string, string>;
  }

  return {
    ...readStoredShareLastRoomMapFromStorage(window.sessionStorage),
    ...readStoredShareLastRoomMapFromStorage(window.localStorage),
  };
}

function readStoredShareLastRoomSessionId(token: string) {
  const normalizedToken = token.trim();

  if (!normalizedToken) {
    return '';
  }

  return readStoredShareLastRoomByToken()[normalizedToken] ?? '';
}

function writeStoredShareLastRoomSessionId(token: string, sessionId: string | null) {
  if (typeof window === 'undefined') {
    return;
  }

  const normalizedToken = token.trim();

  if (!normalizedToken) {
    return;
  }

  const normalizedSessionId = String(sessionId ?? '').trim();
  const nextMap = readStoredShareLastRoomByToken();

  if (normalizedSessionId) {
    nextMap[normalizedToken] = normalizedSessionId;
  } else {
    delete nextMap[normalizedToken];
  }

  const serialized = JSON.stringify(nextMap);
  window.localStorage.setItem(SHARE_LAST_ROOM_STORAGE_KEY, serialized);
  window.sessionStorage.setItem(SHARE_LAST_ROOM_STORAGE_KEY, serialized);
}

function buildShareRoomSnapshotSessionStorageKey(token: string, sessionId: string) {
  return `${SHARE_ROOM_SNAPSHOT_SESSION_STORAGE_KEY_PREFIX}:${token}:${sessionId}`;
}

function canUseShareRoomSnapshotSessionStorage() {
  return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';
}

function readStoredShareRoomSnapshotSessionIndex() {
  if (!canUseShareRoomSnapshotSessionStorage()) {
    return {} as Record<string, Array<{ sessionId: string; savedAt: number }>>;
  }

  try {
    const raw = window.sessionStorage.getItem(SHARE_ROOM_SNAPSHOT_SESSION_INDEX_STORAGE_KEY);

    if (!raw) {
      return {} as Record<string, Array<{ sessionId: string; savedAt: number }>>;
    }

    const parsed = JSON.parse(raw) as Record<string, unknown>;
    return Object.entries(parsed).reduce<Record<string, Array<{ sessionId: string; savedAt: number }>>>((result, [token, entries]) => {
      const normalizedToken = String(token ?? '').trim();

      if (!normalizedToken || !Array.isArray(entries)) {
        return result;
      }

      result[normalizedToken] = entries.flatMap((entry) => {
        if (!entry || typeof entry !== 'object') {
          return [];
        }

        const sessionId = String((entry as { sessionId?: unknown }).sessionId ?? '').trim();
        const savedAt = Number((entry as { savedAt?: unknown }).savedAt);

        if (!sessionId || !Number.isFinite(savedAt) || savedAt <= 0) {
          return [];
        }

        return [{ sessionId, savedAt }];
      });
      return result;
    }, {});
  } catch {
    return {} as Record<string, Array<{ sessionId: string; savedAt: number }>>;
  }
}

function writeStoredShareRoomSnapshotSessionIndex(index: Record<string, Array<{ sessionId: string; savedAt: number }>>) {
  if (!canUseShareRoomSnapshotSessionStorage()) {
    return;
  }

  try {
    const normalizedIndex = Object.entries(index).reduce<Record<string, Array<{ sessionId: string; savedAt: number }>>>((result, [token, entries]) => {
      const normalizedToken = String(token ?? '').trim();

      if (!normalizedToken || !Array.isArray(entries) || entries.length === 0) {
        return result;
      }

      const normalizedEntries = entries
        .map((entry) => ({
          sessionId: String(entry.sessionId ?? '').trim(),
          savedAt: Number(entry.savedAt),
        }))
        .filter((entry) => entry.sessionId && Number.isFinite(entry.savedAt) && entry.savedAt > 0);

      if (normalizedEntries.length > 0) {
        result[normalizedToken] = normalizedEntries;
      }

      return result;
    }, {});

    if (Object.keys(normalizedIndex).length === 0) {
      window.sessionStorage.removeItem(SHARE_ROOM_SNAPSHOT_SESSION_INDEX_STORAGE_KEY);
      return;
    }

    window.sessionStorage.setItem(SHARE_ROOM_SNAPSHOT_SESSION_INDEX_STORAGE_KEY, JSON.stringify(normalizedIndex));
  } catch {
    // Ignore sessionStorage failures and keep runtime fallback active.
  }
}

function resolveShareSnapshotCacheSessionId(snapshot: ChatShareSnapshot | null | undefined) {
  return snapshot?.activeSessionId?.trim() || snapshot?.conversation.sessionId?.trim() || snapshot?.share.sessionId?.trim() || '';
}

function doesShareSnapshotMatchRequestedRoom(
  snapshot: ChatShareSnapshot | null | undefined,
  requestedSessionId: string | null | undefined,
) {
  const normalizedRequestedSessionId = requestedSessionId?.trim() || '';

  if (!normalizedRequestedSessionId) {
    return true;
  }

  return resolveShareSnapshotCacheSessionId(snapshot) === normalizedRequestedSessionId;
}

function readStoredShareRoomSnapshot(token: string, sessionId: string) {
  const normalizedToken = token.trim();
  const normalizedSessionId = sessionId.trim();

  if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId) {
    return null;
  }

  try {
    const raw = window.sessionStorage.getItem(buildShareRoomSnapshotSessionStorageKey(normalizedToken, normalizedSessionId));

    if (!raw) {
      return null;
    }

    const parsed = JSON.parse(raw) as { snapshot?: ChatShareSnapshot | null };
    const snapshot = parsed?.snapshot ?? null;

    if (!snapshot) {
      return null;
    }

    if (snapshot.share.hasAccessPin && !getStoredChatShareAccessPin(normalizedToken)) {
      return null;
    }

    const cachedSessionId = resolveShareSnapshotCacheSessionId(snapshot);
    if (cachedSessionId !== normalizedSessionId) {
      return null;
    }

    return buildShareRoomSwitchCacheSnapshot(snapshot);
  } catch {
    return null;
  }
}

function removeStoredShareRoomSnapshot(token: string, sessionId: string) {
  const normalizedToken = token.trim();
  const normalizedSessionId = sessionId.trim();

  if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId) {
    return;
  }

  try {
    window.sessionStorage.removeItem(buildShareRoomSnapshotSessionStorageKey(normalizedToken, normalizedSessionId));
    const nextIndex = readStoredShareRoomSnapshotSessionIndex();
    const currentEntries = nextIndex[normalizedToken] ?? [];
    const remainingEntries = currentEntries.filter((entry) => entry.sessionId !== normalizedSessionId);

    if (remainingEntries.length > 0) {
      nextIndex[normalizedToken] = remainingEntries;
    } else {
      delete nextIndex[normalizedToken];
    }

    writeStoredShareRoomSnapshotSessionIndex(nextIndex);
  } catch {
    // Ignore sessionStorage failures and keep runtime fallback active.
  }
}

function clearStoredShareRoomSnapshotCache(token: string) {
  const normalizedToken = token.trim();

  if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken) {
    return;
  }

  const nextIndex = readStoredShareRoomSnapshotSessionIndex();
  const currentEntries = nextIndex[normalizedToken] ?? [];
  currentEntries.forEach((entry) => {
    window.sessionStorage.removeItem(buildShareRoomSnapshotSessionStorageKey(normalizedToken, entry.sessionId));
  });
  delete nextIndex[normalizedToken];
  writeStoredShareRoomSnapshotSessionIndex(nextIndex);
}

function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot | null | undefined) {
  const normalizedToken = token.trim();
  const storedSnapshot = buildShareRoomSwitchCacheSnapshot(snapshot ?? null);
  const normalizedSessionId = resolveShareSnapshotCacheSessionId(storedSnapshot);

  if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId || !storedSnapshot) {
    return;
  }

  const nextSavedAt = Date.now();

  try {
    window.sessionStorage.setItem(
      buildShareRoomSnapshotSessionStorageKey(normalizedToken, normalizedSessionId),
      JSON.stringify({
        savedAt: nextSavedAt,
        snapshot: storedSnapshot,
      }),
    );
    const nextIndex = readStoredShareRoomSnapshotSessionIndex();
    const currentEntries = nextIndex[normalizedToken] ?? [];
    const dedupedEntries = [
      { sessionId: normalizedSessionId, savedAt: nextSavedAt },
      ...currentEntries.filter((entry) => entry.sessionId !== normalizedSessionId),
    ]
      .sort((left, right) => right.savedAt - left.savedAt)
      .slice(0, SHARE_ROOM_SNAPSHOT_SESSION_CACHE_LIMIT);

    nextIndex[normalizedToken] = dedupedEntries;
    writeStoredShareRoomSnapshotSessionIndex(nextIndex);

    currentEntries
      .filter((entry) => !dedupedEntries.some((keptEntry) => keptEntry.sessionId === entry.sessionId))
      .forEach((entry) => {
        window.sessionStorage.removeItem(buildShareRoomSnapshotSessionStorageKey(normalizedToken, entry.sessionId));
      });
  } catch {
    // Ignore sessionStorage quota failures and keep network refresh behavior.
  }
}

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 '';
  }

  return new URLSearchParams(window.location.search).get('roomSessionId')?.trim() || '';
}

function writeShareRoomSessionIdToLocation(roomSessionId: string | null, mode: 'push' | 'replace' = 'replace') {
  if (typeof window === 'undefined') {
    return;
  }

  const normalizedSessionId = String(roomSessionId ?? '').trim();
  const nextUrl = new URL(window.location.href);
  const currentSessionId = nextUrl.searchParams.get('roomSessionId')?.trim() || '';

  if (normalizedSessionId) {
    if (currentSessionId === normalizedSessionId) {
      return;
    }

    nextUrl.searchParams.set('roomSessionId', normalizedSessionId);
  } else {
    if (!currentSessionId) {
      return;
    }

    nextUrl.searchParams.delete('roomSessionId');
  }

  const nextPath = `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;

  if (mode === 'push') {
    window.history.pushState(window.history.state, '', nextPath);
    return;
  }

  window.history.replaceState(window.history.state, '', nextPath);
}

function getClientNotificationPermission(): ClientNotificationPermissionState {
  if (
    typeof window === 'undefined'
    || typeof Notification === 'undefined'
    || typeof navigator === 'undefined'
    || !('serviceWorker' in navigator)
    || !('PushManager' in window)
  ) {
    return 'unsupported';
  }

  if (Notification.permission === 'granted') {
    return 'granted';
  }

  if (Notification.permission === 'denied') {
    return 'denied';
  }

  return 'default';
}

function hasSecureOrigin() {
  if (typeof window === 'undefined') {
    return false;
  }

  return window.isSecureContext || window.location.hostname === 'localhost';
}

function isStandaloneDisplayMode() {
  if (typeof window === 'undefined') {
    return false;
  }

  return (
    window.matchMedia?.('(display-mode: standalone)').matches === true ||
    (window.navigator as Navigator & { standalone?: boolean }).standalone === true
  );
}

function isAppleMobileDevice() {
  if (typeof navigator === 'undefined') {
    return false;
  }

  return /iPhone|iPad|iPod/i.test(navigator.userAgent);
}

async function getPushServiceWorkerRegistration() {
  if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
    return null;
  }

  const resolveUsableRegistration = async (registration: ServiceWorkerRegistration | null | undefined) => {
    if (!registration) {
      return null;
    }

    if (registration.active || registration.waiting) {
      return registration;
    }

    const installingWorker = registration.installing;

    if (!installingWorker) {
      return registration;
    }

    await new Promise<void>((resolve) => {
      let completed = false;

      const finish = () => {
        if (completed) {
          return;
        }

        completed = true;
        window.clearTimeout(timeoutId);
        installingWorker.removeEventListener('statechange', onStateChange);
        resolve();
      };

      const onStateChange = () => {
        if (
          installingWorker.state === 'activated'
          || installingWorker.state === 'installed'
          || Boolean(registration.active)
          || Boolean(registration.waiting)
        ) {
          finish();
        }
      };

      const timeoutId = window.setTimeout(finish, 5000);
      installingWorker.addEventListener('statechange', onStateChange);
      onStateChange();
    });

    return registration.active || registration.waiting ? registration : registration;
  };

  const existing = await navigator.serviceWorker.getRegistration();

  if (existing) {
    return (await resolveUsableRegistration(existing)) ?? existing;
  }

  const scoped = await navigator.serviceWorker.getRegistration('/');

  if (scoped) {
    return (await resolveUsableRegistration(scoped)) ?? scoped;
  }

  const knownRegistrations = await navigator.serviceWorker.getRegistrations();

  for (const registration of knownRegistrations) {
    const usableRegistration = await resolveUsableRegistration(registration);

    if (usableRegistration) {
      return usableRegistration;
    }
  }

  const serviceWorkerUrl = import.meta.env.DEV ? '/dev-sw.js?dev-sw' : '/sw.js';
  const resolvedServiceWorkerUrl =
    typeof window !== 'undefined' ? new URL(serviceWorkerUrl, window.location.href).toString() : serviceWorkerUrl;

  try {
    const registration = import.meta.env.DEV
      ? await navigator.serviceWorker.register(resolvedServiceWorkerUrl, { type: 'module', scope: '/', updateViaCache: 'none' })
        .catch(() => navigator.serviceWorker.register(resolvedServiceWorkerUrl, { scope: '/', updateViaCache: 'none' }))
      : await navigator.serviceWorker.register(resolvedServiceWorkerUrl);
    const usableRegistration = await resolveUsableRegistration(registration);

    if (usableRegistration) {
      return usableRegistration;
    }
  } catch {
    // Fall through to ready/getRegistrations checks below.
  }

  try {
    const readyRegistration = await Promise.race([
      navigator.serviceWorker.ready,
      new Promise<null>((resolve) => {
        window.setTimeout(() => resolve(null), 5000);
      }),
    ]);

    if (readyRegistration) {
      return (await resolveUsableRegistration(readyRegistration)) ?? readyRegistration;
    }
  } catch {
    // Ignore and try known registrations one more time.
  }

  const latestRegistrations = await navigator.serviceWorker.getRegistrations();

  for (const registration of latestRegistrations) {
    const usableRegistration = await resolveUsableRegistration(registration);

    if (usableRegistration) {
      return usableRegistration;
    }
  }

  return null;
}

function getClientNotificationPermissionLabel(permission: ClientNotificationPermissionState) {
  switch (permission) {
    case 'granted':
      return '권한 허용';
    case 'denied':
      return '권한 차단';
    case 'unsupported':
      return '브라우저 미지원';
    default:
      return '권한 확인 필요';
  }
}

function buildShareNotificationClientStatus(args: {
  roomEnabled: boolean;
  appEnabled: boolean;
  permission: ClientNotificationPermissionState;
  registrationReady: boolean;
}) {
  const roomLabel = args.roomEnabled ? '방 On' : '방 Off';
  const appLabel = args.appEnabled ? '앱 On' : '앱 Off';
  const permissionLabel = getClientNotificationPermissionLabel(args.permission);
  const registrationLabel = args.registrationReady ? '기기 등록됨' : '기기 미등록';

  if (!args.roomEnabled) {
    return {
      roomLabel,
      appLabel,
      permissionLabel,
      registrationLabel,
      summaryLabel: '이 채팅방 알림 꺼짐',
      tone: 'default',
    } satisfies ShareNotificationClientStatus;
  }

  if (!args.appEnabled) {
    return {
      roomLabel,
      appLabel,
      permissionLabel,
      registrationLabel,
      summaryLabel: '앱 전체 알림 차단',
      tone: 'warning',
    } satisfies ShareNotificationClientStatus;
  }

  if (!hasSecureOrigin()) {
    return {
      roomLabel,
      appLabel,
      permissionLabel,
      registrationLabel,
      summaryLabel: 'HTTPS 환경 필요',
      tone: 'warning',
    } satisfies ShareNotificationClientStatus;
  }

  if (isAppleMobileDevice() && !isStandaloneDisplayMode()) {
    return {
      roomLabel,
      appLabel,
      permissionLabel,
      registrationLabel,
      summaryLabel: 'iPhone PWA 실행 필요',
      tone: 'warning',
    } satisfies ShareNotificationClientStatus;
  }

  if (args.permission !== 'granted') {
    return {
      roomLabel,
      appLabel,
      permissionLabel,
      registrationLabel,
      summaryLabel: args.permission === 'denied' ? '브라우저 권한 차단' : permissionLabel,
      tone: 'warning',
    } satisfies ShareNotificationClientStatus;
  }

  if (!args.registrationReady) {
    return {
      roomLabel,
      appLabel,
      permissionLabel,
      registrationLabel,
      summaryLabel: '현재 기기 등록 필요',
      tone: 'warning',
    } satisfies ShareNotificationClientStatus;
  }

  return {
    roomLabel,
    appLabel,
    permissionLabel,
    registrationLabel,
    summaryLabel: '수신 가능',
    tone: 'success',
  } satisfies ShareNotificationClientStatus;
}

function buildAttachmentMessageBlock(attachments: ChatComposerAttachment[]) {
  if (attachments.length === 0) {
    return '';
  }

  return ['첨부 파일:', ...attachments.map((attachment) => `- ${attachment.name}: ${attachment.publicUrl || attachment.path}`)].join('\n');
}

function buildOutgoingShareMessageText(text: string, attachments: ChatComposerAttachment[]) {
  const trimmedText = text.trim();
  const attachmentBlock = buildAttachmentMessageBlock(attachments);

  if (trimmedText && attachmentBlock) {
    return `${trimmedText}\n\n${attachmentBlock}`;
  }

  return trimmedText || attachmentBlock;
}

function resolveShareComposerPasteFiles(clipboardData: DataTransfer) {
  const clipboardItemFiles = Array.from(clipboardData.items ?? [])
    .filter((item) => item.kind === 'file')
    .map((item) => item.getAsFile())
    .filter((file): file is File => Boolean(file) && file.size > 0);
  const files = Array.from(clipboardData.files ?? []).filter((file) => file.size > 0);
  const candidateFiles = clipboardItemFiles.length > 0 ? clipboardItemFiles : files;

  if (candidateFiles.length > 0) {
    return candidateFiles;
  }

  return [];
}

const COLLAPSIBLE_TEXT_MAX_LENGTH = 720;
const COLLAPSIBLE_TEXT_MAX_LINES = 8;
const DIFF_CODE_BLOCK_PATTERN = /```diff[^\n]*\n([\s\S]*?)```/g;
const SCROLL_JUMP_HIDE_THRESHOLD = 36;
const SCROLL_JUMP_MIN_OVERFLOW = 96;
const SCROLL_JUMP_DIRECTION_THRESHOLD = 2;
const SCROLL_JUMP_IDLE_HIDE_DELAY_MS = 180;
const PROGRAM_MINIMIZED_VIEWPORT_PADDING = 12;
const PROGRAM_MINIMIZED_DEFAULT_WIDTH = 176;
const PROGRAM_MINIMIZED_DEFAULT_HEIGHT = 58;
const SHARE_PROGRAM_MODAL_Z_INDEX = 1750;
const SHARE_PROGRAM_MINIMIZED_Z_INDEX = SHARE_PROGRAM_MODAL_Z_INDEX + 5;
const SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING = 12;
const SHARE_PROCESS_INSPECTOR_DEFAULT_WIDTH = 520;
const SHARE_PROCESS_INSPECTOR_DEFAULT_HEIGHT = 540;
const SHARE_PROCESS_INSPECTOR_FULLSCREEN_WIDTH = 1120;
const SHARE_PROCESS_INSPECTOR_FULLSCREEN_HEIGHT = 820;
const SHARE_PROCESS_INSPECTOR_MINIMIZED_WIDTH = 250;
const SHARE_PROCESS_INSPECTOR_MINIMIZED_HEIGHT = 72;
const SHARE_PROCESS_INSPECTOR_Z_INDEX = SHARE_PROGRAM_MINIMIZED_Z_INDEX + 5;
const MOBILE_INPUT_VIEWPORT_TOP_PADDING_PX = 6;
const MOBILE_INPUT_VIEWPORT_BOTTOM_PADDING_PX = 8;
const MOBILE_INPUT_VIEWPORT_SYNC_RETRY_DELAYS_MS = [180, 360] as const;
const renderAccessPinVisibilityIcon = (visible: boolean) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />);

function areStringListsEqual(left: string[], right: string[]) {
  return left.length === right.length && left.every((value, index) => value === right[index]);
}

function resolveShareRoomDefaultContextIds(
  roomContextSettings: ReturnType<typeof resolveChatRoomContextSettings>,
  chatTypeDefaults: Parameters<typeof resolveChatTypeDefaultContextIds>[0],
  chatTypeId: string | null | undefined,
) {
  const inheritedDefaultContextIds = resolveChatTypeDefaultContextIds(chatTypeDefaults, chatTypeId);
  const roomDefaultContextIds = roomContextSettings?.defaultContextIds ?? [];

  return roomDefaultContextIds.length > 0 ? roomDefaultContextIds : inheritedDefaultContextIds;
}
const SHARE_EXPAND_MODE_LABELS: Record<ShareExpandMode, string> = {
  latest: '마지막건',
  pending: '처리중·미확인',
  all: '전체',
};
const TOKEN_USAGE_PERIODS = [
  { key: '7d', label: '1주일', windowMs: 7 * 24 * 60 * 60 * 1000 },
  { key: '5h', label: '5시간', windowMs: 5 * 60 * 60 * 1000 },
] as const;
const SHARE_APP_ENVIRONMENT_OPTIONS: Array<{
  key: ShareAppEnvironment;
  label: string;
  origin: string;
}> = [
  { key: 'preview', label: 'preview', origin: 'https://preview.sm-home.cloud' },
  { key: 'test', label: 'test', origin: 'https://test.sm-home.cloud' },
  { key: 'prod', label: 'prod', origin: 'https://sm-home.cloud' },
] as const;
const SHARE_APP_LAUNCH_USAGE_STORAGE_KEY = 'chat-share-page:app-launch-usage:v1';
const CHAT_SHARE_INSTALL_NAME = '리소스 공유 채팅방';
const CHAT_SHARE_INSTALL_SHORT_NAME = '공유채팅';
const CHAT_SHARE_INSTALL_THEME_COLOR = '#165dff';
const SHARE_CURRENT_CHAT_APP_ID = 'shared-chat-current';

type TokenUsagePeriodKey = (typeof TOKEN_USAGE_PERIODS)[number]['key'];

type TokenUsageWindowSummary = {
  totalUsedTokens: number;
  inputTokens: number;
  outputTokens: number;
  cachedTokens: number;
  reasoningTokens: number;
  requestCount: number;
  percentage: number | null;
  remainingTokens: number | null;
  windowStartedAt: string | null;
  nextUsageDropAt: string | null;
  fullResetAt: string | null;
  nextResetTokens: number;
};
const APPS_LAUNCHER_SEARCH_TERMS = ['apps', 'app', '앱', '프로그램', '실행', '빠른 실행'];
const SHARE_MANAGEMENT_APP_OPTIONS = [
  {
    value: 'text-memo-widget',
    label: '메모',
    description: '공유채팅 안에서 메모 컴포넌트를 바로 실행',
    icon: <FileTextOutlined />,
    usagePriority: 70,
  },
  {
    value: 'token-setting',
    label: '토큰관리 설정',
    description: '토큰 설정 목록과 상세 편집 접근',
    icon: <SettingOutlined />,
    usagePriority: 55,
  },
  {
    value: 'shared-resource',
    label: '공유 리소스 관리',
    description: '공유 링크와 권한, 활동 이력 관리',
    icon: <AppstoreOutlined />,
    usagePriority: 50,
  },
  {
    value: 'app-settings',
    label: '앱 설정',
    description: '헤더 설정, 알림, 업데이트 화면 접근',
    icon: <SettingOutlined />,
    usagePriority: 75,
  },
  {
    value: 'server-command',
    label: '서버관리',
    description: '여러 서버 상태 확인과 재기동 관리 화면 접근',
    icon: <ReloadOutlined />,
    usagePriority: 95,
  },
] as const;

type ShareAppLaunchUsageMap = Record<string, { count: number; lastOpenedAt: number }>;

function readShareAppLaunchUsage(): ShareAppLaunchUsageMap {
  if (typeof window === 'undefined') {
    return {};
  }

  try {
    const rawValue = window.localStorage.getItem(SHARE_APP_LAUNCH_USAGE_STORAGE_KEY);
    if (!rawValue) {
      return {};
    }

    const parsedValue = JSON.parse(rawValue) as unknown;
    if (!parsedValue || typeof parsedValue !== 'object') {
      return {};
    }

    return Object.entries(parsedValue as Record<string, unknown>).reduce<ShareAppLaunchUsageMap>((accumulator, [key, value]) => {
      if (!value || typeof value !== 'object') {
        return accumulator;
      }

      const count = Number((value as { count?: number }).count);
      const lastOpenedAt = Number((value as { lastOpenedAt?: number }).lastOpenedAt);
      if (!Number.isFinite(count) || count < 0 || !Number.isFinite(lastOpenedAt) || lastOpenedAt < 0) {
        return accumulator;
      }

      accumulator[key] = { count, lastOpenedAt };
      return accumulator;
    }, {});
  } catch {
    return {};
  }
}

function writeShareAppLaunchUsage(value: ShareAppLaunchUsageMap) {
  if (typeof window === 'undefined') {
    return;
  }

  try {
    window.localStorage.setItem(SHARE_APP_LAUNCH_USAGE_STORAGE_KEY, JSON.stringify(value));
  } catch {
    // Ignore storage failures and keep the UI usable.
  }
}

function resolveShareAppUsageBadge(record?: { count: number; lastOpenedAt: number } | null) {
  if (!record || record.count < 2) {
    return null;
  }

  return record.count >= 5 ? '자주 사용' : '최근 사용';
}

function compareShareAppLaunchOrder(
  leftId: string,
  rightId: string,
  leftPriority: number,
  rightPriority: number,
  usageMap: ShareAppLaunchUsageMap,
) {
  const leftUsage = usageMap[leftId];
  const rightUsage = usageMap[rightId];
  const leftCount = leftUsage?.count ?? 0;
  const rightCount = rightUsage?.count ?? 0;
  if (leftCount !== rightCount) {
    return rightCount - leftCount;
  }

  const leftLastOpenedAt = leftUsage?.lastOpenedAt ?? 0;
  const rightLastOpenedAt = rightUsage?.lastOpenedAt ?? 0;
  if (leftLastOpenedAt !== rightLastOpenedAt) {
    return rightLastOpenedAt - leftLastOpenedAt;
  }

  if (leftPriority !== rightPriority) {
    return rightPriority - leftPriority;
  }

  return leftId.localeCompare(rightId, 'ko');
}

function buildShareAllowedAppIdSet(allowedAppIds: string[] | null | undefined, permissions: string[] | null | undefined) {
  const normalizedAllowedAppIds = new Set(
    (allowedAppIds ?? [])
      .map((item) => item.trim().toLowerCase())
      .filter(Boolean),
  );
  const normalizedPermissions = new Set(
    (permissions ?? [])
      .map((item) => item.trim().toLowerCase())
      .filter(Boolean),
  );

  return normalizedAllowedAppIds;
}

function buildSharePromptAnchorKey(messageId: number, promptIndex: number) {
  return `${messageId}:${promptIndex}`;
}

function scrollToShareAnchorElement(element: HTMLElement | null | undefined) {
  if (!element) {
    return false;
  }

  element.scrollIntoView({
    behavior: 'smooth',
    block: 'start',
    inline: 'nearest',
  });
  return true;
}

function buildPhotoPrismProgramTarget(): ShareProgramTarget {
  return {
    key: 'launcher:photoprism',
    label: 'PhotoPrism',
    url: '/play/apps?app=photoprism&launchContext=embedded',
    kind: 'document',
    meta: '임시 프로그램 실행',
    appId: 'photoprism',
  };
}

function buildPlayAppProgramTarget(appId: string, label: string): ShareProgramTarget {
  return {
    key: `launcher:${appId}`,
    label,
    url: buildPlayAppPath(appId, 'embedded'),
    kind: 'document',
    meta: '허용된 Apps 실행',
    appId,
  };
}

function buildShareManagementProgramTarget(appId: string, label: string): ShareProgramTarget {
  return {
    key: `management:${appId}`,
    label,
    url: buildChatPath('live'),
    kind: 'document',
    meta: '공유채팅 관리 앱 실행',
    appId,
  };
}

function resolveShareWorkServerVersionStatus(item: ServerCommandItem | null): ShareWorkServerVersionStatus {
  if (!item) {
    return 'unknown';
  }

  if (item.buildRequired) {
    return 'build-required';
  }

  if (item.updateAvailable) {
    return 'update-available';
  }

  return 'latest';
}

function resolveShareWorkServerVersionLabel(item: ServerCommandItem | null) {
  const latestVersion = item?.latestVersion?.trim();
  const runningVersion = item?.runningVersion?.trim();

  if (latestVersion) {
    return latestVersion;
  }

  if (runningVersion) {
    return runningVersion;
  }

  return '확인 필요';
}

function resolveShareWorkServerStatusLabel(item: ServerCommandItem | null) {
  const status = resolveShareWorkServerVersionStatus(item);

  if (status === 'build-required') {
    return '빌드 필요';
  }

  if (status === 'update-available') {
    return '반영 대기';
  }

  if (status === 'latest') {
    return '최신';
  }

  return '확인 필요';
}

function resolveShareWorkServerVersionDescription(item: ServerCommandItem | null) {
  if (!item) {
    return 'WORK 서버 상태를 아직 확인하지 못했습니다.';
  }

  if (item.buildRequired) {
    return '소스 변경이 있어 최신 빌드가 더 필요합니다.';
  }

  if (item.updateAvailable) {
    return '새 빌드는 준비됐고 현재 런타임 반영만 남아 있습니다.';
  }

  return '무중단 배포 예약과 최신 런타임 상태를 확인합니다.';
}

function buildPlayAppEnvironmentUrl(appId: string, environment: ShareAppEnvironment) {
  const origin = SHARE_APP_ENVIRONMENT_OPTIONS.find((item) => item.key === environment)?.origin ?? SHARE_APP_ENVIRONMENT_OPTIONS[0].origin;
  return new URL(buildPlayAppPath(appId, 'embedded'), origin).toString();
}

function buildShareChatEnvironmentUrl(
  sharePath: string | null | undefined,
  shareToken: string | null | undefined,
  environment: ShareAppEnvironment,
) {
  const origin = SHARE_APP_ENVIRONMENT_OPTIONS.find((item) => item.key === environment)?.origin ?? SHARE_APP_ENVIRONMENT_OPTIONS[0].origin;
  const normalizedSharePath = sharePath?.trim() ?? '';
  const normalizedShareToken = shareToken?.trim() ?? '';
  const pathname = normalizedSharePath || (normalizedShareToken ? `/chat/share/${encodeURIComponent(normalizedShareToken)}` : '/chat/live');
  return new URL(pathname, origin).toString();
}

function buildSharePlayAppInstallPath(appId: string, shareToken?: string | null) {
  const installPath = new URL(buildPlayAppPath(appId), 'https://preview.sm-home.cloud');
  const normalizedShareToken = shareToken?.trim() ?? '';

  if (appId === 'e-reader' && normalizedShareToken) {
    installPath.searchParams.set('shareToken', normalizedShareToken);
  }

  return `${installPath.pathname}${installPath.search}${installPath.hash}`;
}

function resolveSharePlayAppInstallThemeColor(appId: string) {
  switch (appId) {
    case 'baseball-ticket-bay':
      return '#1b3f91';
    case 'photoprism':
      return '#0f766e';
    case 'photo-puzzle':
      return '#d97706';
    case 'the-quest':
      return '#7c3aed';
    case 'template1':
      return '#2f8dff';
    case 'tetris':
      return '#0f172a';
    default:
      return '#165dff';
  }
}

function resolveShareAppEnvironmentFromOrigin(origin?: string | null): ShareAppEnvironment {
  const normalizedOrigin = origin?.trim().toLowerCase();

  if (!normalizedOrigin) {
    return 'preview';
  }

  return SHARE_APP_ENVIRONMENT_OPTIONS.find((item) => item.origin.toLowerCase() === normalizedOrigin)?.key ?? 'preview';
}

function buildPlayAppEnvironmentTarget(
  appId: string,
  label: string,
  environment: ShareAppEnvironment,
): ShareProgramTarget {
  const environmentLabel = SHARE_APP_ENVIRONMENT_OPTIONS.find((item) => item.key === environment)?.label ?? environment;

  return {
    key: `launcher:${environment}:${appId}`,
    label,
    url: buildPlayAppEnvironmentUrl(appId, environment),
    kind: 'document',
    meta: `허용된 Apps 실행 · ${environmentLabel}`,
    appId,
  };
}

function buildShareChatEnvironmentTarget(
  sharePath: string | null | undefined,
  shareToken: string | null | undefined,
  environment: ShareAppEnvironment,
): ShareProgramTarget {
  const environmentLabel = SHARE_APP_ENVIRONMENT_OPTIONS.find((item) => item.key === environment)?.label ?? environment;

  return {
    key: `launcher:${environment}:${SHARE_CURRENT_CHAT_APP_ID}`,
    label: '채팅',
    url: buildShareChatEnvironmentUrl(sharePath, shareToken, environment),
    kind: 'document',
    meta: `현재 공유토큰 열기 · ${environmentLabel}`,
    appId: SHARE_CURRENT_CHAT_APP_ID,
  };
}

function shouldRenderSharePlayAppInline(target: ShareProgramTarget | null | undefined) {
  if (!target?.appId || !findReadyPlayAppEntryById(target.appId)) {
    return false;
  }

  if (typeof window === 'undefined') {
    return true;
  }

  try {
    return new URL(target.url, window.location.origin).origin === window.location.origin;
  } catch {
    return true;
  }
}

function resolveSupportedEnvironmentSummary(entry: PlayAppEntry) {
  return getSupportedPlayAppEnvironments(entry).join(', ');
}

function resolveFirstSupportedShareAppEnvironment(entries: PlayAppEntry[]) {
  for (const option of SHARE_APP_ENVIRONMENT_OPTIONS) {
    if (entries.some((entry) => isPlayAppSupportedInEnvironment(entry, option.key))) {
      return option.key;
    }
  }

  return 'preview';
}

function renderEmbeddedSharePlayApp(appId: string | undefined, onBack: () => void, shareToken?: string | null) {
  if (appId === 'photoprism') {
    return <PhotoPrismAppView onBack={onBack} launchContext="embedded" />;
  }

  if (appId === 'baseball-ticket-bay') {
    return <BaseballTicketBayPlayAppView onBack={onBack} launchContext="embedded" shareToken={shareToken} />;
  }

  if (appId === 'e-reader') {
    return <EReaderAppView onBack={onBack} launchContext="embedded" shareToken={shareToken} />;
  }

  if (appId === 'photo-puzzle') {
    return <PhotoPuzzleAppView onBack={onBack} launchContext="embedded" />;
  }

  if (appId === 'tetris') {
    return <TetrisAppView onBack={onBack} launchContext="embedded" />;
  }

  if (appId === 'the-quest') {
    return <TheQuestAppView onBack={onBack} launchContext="embedded" />;
  }

  if (appId === 'template1') {
    return <Template1PlayAppView onBack={onBack} launchContext="embedded" />;
  }

  return null;
}

function formatTimeLabel(value?: string | null) {
  const normalizedValue = value?.trim();

  if (!normalizedValue) {
    return '';
  }

  const parsedDate = new Date(normalizedValue);

  if (Number.isNaN(parsedDate.getTime())) {
    return normalizedValue;
  }

  return parsedDate.toLocaleString('ko-KR', {
    hour12: false,
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
  });
}

function formatRemainingTimeLabel(expiresAt: string, nowMs: number) {
  const expiresAtMs = new Date(expiresAt).getTime();

  if (!Number.isFinite(expiresAtMs)) {
    return '';
  }

  const diffMs = expiresAtMs - nowMs;

  if (diffMs <= 0) {
    return '남은시간 없음';
  }

  const totalMinutes = Math.floor(diffMs / (60 * 1000));
  const days = Math.floor(totalMinutes / (24 * 60));
  const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
  const minutes = totalMinutes % 60;
  const parts: string[] = [];

  if (days > 0) {
    parts.push(`${days}일`);
  }

  if (hours > 0) {
    parts.push(`${hours}시간`);
  }

  if (minutes > 0 || parts.length === 0) {
    parts.push(`${minutes}분`);
  }

  return `남은시간 ${parts.join(' ')}`;
}

function formatCountdownLabel(targetAt: string | null | undefined, nowMs: number) {
  const normalizedValue = targetAt?.trim();

  if (!normalizedValue) {
    return '예정 없음';
  }

  const targetMs = new Date(normalizedValue).getTime();

  if (!Number.isFinite(targetMs)) {
    return '시간 미기록';
  }

  const diffMs = Math.max(0, targetMs - nowMs);
  const totalSeconds = Math.floor(diffMs / 1000);
  const days = Math.floor(totalSeconds / (24 * 60 * 60));
  const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
  const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
  const seconds = totalSeconds % 60;
  const timeLabel = [hours, minutes, seconds].map((value) => String(value).padStart(2, '0')).join(':');

  if (days > 0) {
    return `${days}일 ${timeLabel}`;
  }

  return timeLabel;
}

function formatDurationMinutesLabel(totalMinutes: number | null | undefined) {
  const normalizedMinutes = Number(totalMinutes ?? 0);

  if (!Number.isFinite(normalizedMinutes)) {
    return '';
  }

  if (normalizedMinutes <= 0) {
    return '무제한';
  }

  const roundedMinutes = Math.max(1, Math.round(normalizedMinutes));
  const days = Math.floor(roundedMinutes / (24 * 60));
  const hours = Math.floor((roundedMinutes % (24 * 60)) / 60);
  const minutes = roundedMinutes % 60;
  const parts: string[] = [];

  if (days > 0) {
    parts.push(`${days}일`);
  }

  if (hours > 0) {
    parts.push(`${hours}시간`);
  }

  if (minutes > 0 || parts.length === 0) {
    parts.push(`${minutes}분`);
  }

  return parts.join(' ');
}

function formatShareExpirySummary(expiresAt: string | null | undefined, nowMs: number) {
  const normalizedValue = expiresAt?.trim();

  if (!normalizedValue) {
    return '사용기간 제한 없음';
  }

  const formattedTime = formatTimeLabel(normalizedValue);
  const remainingTime = formatRemainingTimeLabel(normalizedValue, nowMs);
  const baseLabel = formattedTime || normalizedValue;

  if (remainingTime) {
    return `만료 ${baseLabel} · ${remainingTime}`;
  }

  return `만료 ${baseLabel}`;
}

function resolveShareEffectiveExpiresAt(share: ChatShareSnapshot['share'] | null | undefined) {
  const explicitExpiresAt = share?.expiresAt?.trim();

  if (explicitExpiresAt) {
    return explicitExpiresAt;
  }

  const defaultExpiresInMinutes = Number(share?.tokenSetting?.defaultExpiresInMinutes ?? 0);
  const createdAtMs = Date.parse(share?.createdAt?.trim() ?? '');

  if (!Number.isFinite(defaultExpiresInMinutes) || defaultExpiresInMinutes <= 0 || !Number.isFinite(createdAtMs)) {
    return '';
  }

  return new Date(createdAtMs + defaultExpiresInMinutes * 60 * 1000).toISOString();
}

function buildAbsoluteShareUrl(path?: string | null) {
  const normalizedPath = path?.trim();

  if (!normalizedPath) {
    return '';
  }

  if (/^https?:\/\//i.test(normalizedPath)) {
    return normalizedPath;
  }

  if (typeof window === 'undefined') {
    return normalizedPath;
  }

  return new URL(normalizedPath, window.location.origin).toString();
}

function createChatShareManifestHref(pathname: string, title?: string | null) {
  const normalizedTitle = title?.trim();
  return createInstallManifestObjectUrl({
    startPath: pathname,
    scope: pathname,
    name: normalizedTitle || CHAT_SHARE_INSTALL_NAME,
    shortName: CHAT_SHARE_INSTALL_SHORT_NAME,
    description: '리소스 공유 채팅방을 홈 화면 앱으로 바로 엽니다.',
    themeColor: CHAT_SHARE_INSTALL_THEME_COLOR,
    backgroundColor: '#f4f7fb',
  });
}

function swapManifestForChatShare(manifestHref: string, title?: string | null) {
  return swapInstallDocumentMetadata({
    manifestHref,
    title: title?.trim() || CHAT_SHARE_INSTALL_NAME,
    themeColor: CHAT_SHARE_INSTALL_THEME_COLOR,
  });
}

function getShareExpandModeLabel(mode: ShareExpandMode) {
  return SHARE_EXPAND_MODE_LABELS[mode];
}

function normalizeSearchKeyword(value: string) {
  return value.trim().toLocaleLowerCase('ko-KR');
}

function isShareInteractivePointerTarget(target: EventTarget | null) {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  return Boolean(target.closest('button, a, input, textarea, select, [role="button"], [data-no-drag="true"]'));
}

function formatTokenCount(value: number) {
  return Math.max(0, Math.round(Number(value) || 0)).toLocaleString('ko-KR');
}

function resolveSmallestFiniteNumber(...values: Array<number | null | undefined>) {
  const normalizedValues = values.filter((value): value is number => typeof value === 'number' && Number.isFinite(value));

  if (normalizedValues.length === 0) {
    return null;
  }

  return Math.min(...normalizedValues);
}

function resolveRequestUsageAt(request: ChatConversationRequest) {
  const candidates = [request.answeredAt, request.terminalAt, request.updatedAt, request.createdAt];

  for (const candidate of candidates) {
    const normalized = candidate?.trim();

    if (!normalized) {
      continue;
    }

    const parsed = new Date(normalized).getTime();

    if (Number.isFinite(parsed)) {
      return parsed;
    }
  }

  return 0;
}

function resolveRequestMessageTimestamp(request: ChatConversationRequest) {
  const candidates = [request.answeredAt, request.terminalAt, request.updatedAt, request.createdAt];

  for (const candidate of candidates) {
    const normalized = candidate?.trim();

    if (!normalized) {
      continue;
    }

    return normalized;
  }

  return '';
}

function resolveRequestUsageTokens(request: ChatConversationRequest) {
  return Math.max(
    0,
    Math.round(
      Number(
        request.usageSnapshot?.tokenTotals?.total ??
          request.usageSnapshot?.totalTokens ??
          request.totalTokens ??
          0,
      ) || 0,
    ),
  );
}

function resolveRequestCodexModel(request: ChatConversationRequest) {
  return normalizeCodexModel(request.codexModel?.trim() || '');
}

function resolveTokenUsageWindowSummary(
  requests: ChatConversationRequest[],
  periodKey: TokenUsagePeriodKey,
  nowMs: number,
  limit: number,
): TokenUsageWindowSummary {
  const period = TOKEN_USAGE_PERIODS.find((item) => item.key === periodKey) ?? TOKEN_USAGE_PERIODS[0];
  const threshold = nowMs - period.windowMs;
  const matchingRequests = requests.filter((request) => resolveRequestUsageAt(request) >= threshold);
  const tokenAccumulator = matchingRequests.reduce(
    (accumulator, request) => {
      accumulator.totalUsedTokens += resolveRequestUsageTokens(request);
      accumulator.inputTokens += Math.max(0, Math.round(Number(request.usageSnapshot?.tokenTotals?.input ?? 0) || 0));
      accumulator.outputTokens += Math.max(0, Math.round(Number(request.usageSnapshot?.tokenTotals?.output ?? 0) || 0));
      accumulator.cachedTokens += Math.max(0, Math.round(Number(request.usageSnapshot?.tokenTotals?.cached ?? 0) || 0));
      accumulator.reasoningTokens += Math.max(0, Math.round(Number(request.usageSnapshot?.tokenTotals?.reasoning ?? 0) || 0));
      return accumulator;
    },
    {
      totalUsedTokens: 0,
      inputTokens: 0,
      outputTokens: 0,
      cachedTokens: 0,
      reasoningTokens: 0,
    },
  );
  const usageResetSchedule = matchingRequests
    .map((request) => resolveRequestUsageAt(request) + period.windowMs)
    .filter((value) => Number.isFinite(value))
    .sort((left, right) => left - right);
  const nextUsageDropAtMs = usageResetSchedule.length > 0 ? usageResetSchedule[0] : null;
  const normalizedLimit = Math.max(0, Math.round(Number(limit) || 0));
  const percentage = normalizedLimit > 0 ? Math.min(100, (tokenAccumulator.totalUsedTokens / normalizedLimit) * 100) : null;
  const remainingTokens = normalizedLimit > 0 ? Math.max(0, normalizedLimit - tokenAccumulator.totalUsedTokens) : null;
  const nextResetTokens =
    nextUsageDropAtMs == null
      ? 0
      : matchingRequests.reduce((accumulator, request) => {
          if (resolveRequestUsageAt(request) + period.windowMs !== nextUsageDropAtMs) {
            return accumulator;
          }

          return accumulator + resolveRequestUsageTokens(request);
        }, 0);

  return {
    ...tokenAccumulator,
    requestCount: matchingRequests.length,
    percentage,
    remainingTokens,
    windowStartedAt: new Date(threshold).toISOString(),
    nextUsageDropAt: nextUsageDropAtMs != null ? new Date(nextUsageDropAtMs).toISOString() : null,
    fullResetAt: usageResetSchedule.length > 0 ? new Date(usageResetSchedule[usageResetSchedule.length - 1]).toISOString() : null,
    nextResetTokens,
  };
}

function resolveTokenUsageLimitForPeriod(
  setting:
    | {
        maxTokensPer30Days: number;
        maxTokensPer7Days: number;
        maxTokensPer5Hours: number;
      }
    | null
    | undefined,
  periodKey: TokenUsagePeriodKey,
) {
  if (!setting) {
    return 0;
  }

  if (periodKey === '7d') {
    return setting.maxTokensPer7Days;
  }

  if (periodKey === '5h') {
    return setting.maxTokensPer5Hours;
  }

  return setting.maxTokensPer7Days;
}

function clampProgramMinimizedValue(value: number, min: number, max: number) {
  if (max < min) {
    return min;
  }

  return Math.min(Math.max(value, min), max);
}

function resetMobileDocumentScrollOffset() {
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return;
  }

  const documentElement = document.documentElement;
  const body = document.body;
  const visualViewport = window.visualViewport;
  const layoutScrollTop = Math.max(window.scrollY || 0, documentElement?.scrollTop || 0, body?.scrollTop || 0);
  const visualOffsetTop = Math.max(visualViewport?.offsetTop ?? 0, visualViewport?.pageTop ?? 0);

  if (layoutScrollTop <= 0 && visualOffsetTop <= 0) {
    return;
  }

  window.scrollTo(0, 0);

  if (documentElement) {
    documentElement.scrollTop = 0;
  }

  if (body) {
    body.scrollTop = 0;
  }
}

function isMobileShareInputTarget(target: EventTarget | null): target is HTMLElement {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  const inputTarget = target.closest<HTMLElement>('textarea, input, [contenteditable="true"]');
  return Boolean(inputTarget) && isSnapshotDeferrableFocusTarget(inputTarget);
}

function isMobileTouchViewport() {
  if (typeof window === 'undefined') {
    return false;
  }

  return window.innerWidth <= 768
    && (window.matchMedia('(pointer: coarse)').matches || window.matchMedia('(hover: none)').matches);
}

function isShareEdgeGestureIgnoredTarget(target: EventTarget | null) {
  if (isTypingTarget(target)) {
    return true;
  }

  if (!(target instanceof HTMLElement)) {
    return false;
  }

  return Boolean(
    target.closest(
      'button, a, summary, [role="button"], [data-share-edge-gesture-ignore], .ant-modal-root, .ant-drawer, .ant-dropdown, .ant-select-dropdown, .chat-share-page__program-shell, .chat-share-page__program-minimized',
    ),
  );
}

function getDefaultProgramMinimizedPosition() {
  if (typeof window === 'undefined') {
    return { x: PROGRAM_MINIMIZED_VIEWPORT_PADDING, y: PROGRAM_MINIMIZED_VIEWPORT_PADDING };
  }

  return {
    x: Math.max(PROGRAM_MINIMIZED_VIEWPORT_PADDING, window.innerWidth - PROGRAM_MINIMIZED_DEFAULT_WIDTH - PROGRAM_MINIMIZED_VIEWPORT_PADDING),
    y: Math.max(PROGRAM_MINIMIZED_VIEWPORT_PADDING, window.innerHeight - PROGRAM_MINIMIZED_DEFAULT_HEIGHT - PROGRAM_MINIMIZED_VIEWPORT_PADDING),
  };
}

function getStackedProgramMinimizedPosition(index: number) {
  const basePosition = getDefaultProgramMinimizedPosition();
  const offsetX = 18 * Math.max(0, index);
  const offsetY = 14 * Math.max(0, index);

  return {
    x: Math.max(PROGRAM_MINIMIZED_VIEWPORT_PADDING, basePosition.x - offsetX),
    y: Math.max(PROGRAM_MINIMIZED_VIEWPORT_PADDING, basePosition.y - offsetY),
  };
}

function getDefaultShareProcessInspectorPosition() {
  if (typeof window === 'undefined') {
    return { x: SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING, y: SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING };
  }

  const width = Math.min(SHARE_PROCESS_INSPECTOR_DEFAULT_WIDTH, window.innerWidth - SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING * 2);
  const centeredX = Math.round((window.innerWidth - width) / 2);

  return {
    x: Math.max(SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING, centeredX),
    y: Math.max(SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING, 88),
  };
}

function matchesSearchKeyword(keyword: string, ...values: Array<string | null | undefined>) {
  if (!keyword) {
    return true;
  }

  return values.some((value) => normalizeSearchKeyword(value ?? '').includes(keyword));
}

function buildSearchKeywordText(...values: Array<string | null | undefined>) {
  return normalizeSearchKeyword(values.filter(Boolean).join(' '));
}

function buildSharePromptSelectionKey(parentRequestId: string, sourceMessageId: number, promptIndex: number, promptTitle: string, promptSignature: string) {
  return [parentRequestId.trim(), sourceMessageId, promptIndex, promptTitle.trim(), promptSignature.trim()].join('::');
}

function extractShareMessageRenderPayload(message: ChatMessage): ShareMessageRenderPayload {
  const extracted = extractChatMessageParts(message.text);
  const promptParts = (message.parts ?? []).filter(
    (part): part is Extract<ChatMessagePart, { type: 'prompt' }> => part.type === 'prompt',
  );
  const strippedPromptText = extracted.strippedText || message.text;
  const diffBlocks = Array.from(strippedPromptText.matchAll(DIFF_CODE_BLOCK_PATTERN))
    .map((match) => match[1]?.trim())
    .filter((value): value is string => Boolean(value));
  const visibleText = stripHiddenPreviewTags(strippedPromptText.replace(DIFF_CODE_BLOCK_PATTERN, '')).trim();

  return {
    visibleText,
    promptParts:
      promptParts.length > 0
        ? promptParts
        : extracted.parts.filter((part): part is Extract<ChatMessagePart, { type: 'prompt' }> => part.type === 'prompt'),
    diffBlocks,
    previewItems: extractPreviewItems([message]),
  };
}

function resolveRenderedMessage(message: ChatMessage): ShareRenderedMessage {
  return extractShareMessageRenderPayload(message);
}

function buildShareRequestMessagesById(messages: ChatMessage[]) {
  const nextMap = new Map<string, ChatMessage[]>();

  messages.forEach((message) => {
    const requestId = message.clientRequestId?.trim() || '';

    if (!requestId) {
      return;
    }

    const current = nextMap.get(requestId) ?? [];
    current.push(message);
    nextMap.set(requestId, current);
  });

  return nextMap;
}

function resolveShareRoomPendingCounts(snapshot: Pick<ChatShareSnapshot, 'requests' | 'messages'>): ShareRoomPendingCounts {
  const sortedRequests = [...snapshot.requests].sort(compareShareConversationRequests);
  const sortedMessages = [...snapshot.messages].sort((left, right) => left.id - right.id);
  const messageRenderPayloadById = new Map(
    sortedMessages.map((message) => [message.id, extractShareMessageRenderPayload(message)] as const),
  );
  const requestMessagesById = buildShareRequestMessagesById(sortedMessages);
  const childRequestCountByParentId = buildShareChildRequestCountMap(sortedRequests);
  const promptFollowupCountByParentId = buildSharePromptFollowupCountMap(sortedRequests);
  const pendingCompletionRequests = sortedRequests.filter((request) =>
    isPendingCompletionShareRequest(
      request,
      requestMessagesById.get(request.requestId) ?? [],
      childRequestCountByParentId,
      promptFollowupCountByParentId,
      messageRenderPayloadById,
    ));
  const processingCount = pendingCompletionRequests.filter((request) => isRequestInFlight(request.status)).length;

  return {
    processingCount,
    unansweredCount: Math.max(0, pendingCompletionRequests.length - processingCount),
  };
}

function buildVisibleMessageText(message: ChatMessage, payload?: ShareMessageRenderPayload) {
  return (payload ?? resolveRenderedMessage(message)).visibleText.trim();
}

const SHARE_ATTACHMENT_ACCEPT = 'image/*,.heic,.heif,.zip,application/zip,application/x-zip-compressed';

function isCollapsibleText(value: string) {
  const normalized = value.trim();

  if (!normalized) {
    return false;
  }

  if (normalized.length > COLLAPSIBLE_TEXT_MAX_LENGTH) {
    return true;
  }

  return normalized.split(/\r?\n/).length > COLLAPSIBLE_TEXT_MAX_LINES;
}

function isShareSendDelayError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error ?? '');
  return /지연|timeout|연결/i.test(message);
}

function buildRequestAnswerText(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  const relatedSegments = relatedMessages
    .filter((message) => message.author !== 'user')
    .map((message) => buildVisibleMessageText(message, messageRenderPayloadById?.get(message.id)))
    .filter(Boolean);

  if (relatedSegments.length > 0) {
    return relatedSegments.join('\n\n').trim();
  }

  return stripHiddenPreviewTags(String(request.responseText ?? '').replace(DIFF_CODE_BLOCK_PATTERN, '')).trim();
}

function resolveShareRequestFallbackAnswerText(request: ChatConversationRequest) {
  if (request.status === 'queued') {
    return '요청 대기 등록 하였습니다.';
  }

  if (request.status === 'accepted' || request.status === 'started') {
    return '요청 처리 중 입니다.';
  }

  return request.statusMessage?.trim() || '아직 답변이 없습니다.';
}

function replaceChatShareSnapshotRequest(snapshot: ChatShareSnapshot, nextRequest: ChatConversationRequest): ChatShareSnapshot {
  return {
    ...snapshot,
    targetRequest:
      snapshot.targetRequest.requestId === nextRequest.requestId
        ? nextRequest
        : snapshot.targetRequest,
    requests: snapshot.requests.map((request) =>
      request.requestId === nextRequest.requestId ? nextRequest : request,
    ),
  };
}

function mergeChatShareSnapshotHistory(
  snapshot: ChatShareSnapshot,
  detailPage: {
    requests: ChatConversationRequest[];
    messages: ChatMessage[];
    activityLogs: ChatShareSnapshot['activityLogs'];
    oldestLoadedMessageId: number | null;
    hasOlderMessages: boolean;
  },
): ChatShareSnapshot {
  const mergedRequests = [...detailPage.requests, ...snapshot.requests];
  const mergedMessages = [...detailPage.messages, ...snapshot.messages];
  const mergedActivityLogs = [...detailPage.activityLogs, ...snapshot.activityLogs];

  return {
    ...snapshot,
    detailLevel: detailPage.hasOlderMessages ? 'initial' : 'full',
    requests: mergedRequests.filter(
      (request, index, array) => array.findIndex((item) => item.requestId === request.requestId) === index,
    ),
    messages: mergedMessages.filter(
      (message, index, array) => array.findIndex((item) => item.id === message.id) === index,
    ),
    activityLogs: mergedActivityLogs.filter(
      (activityLog, index, array) => array.findIndex((item) => item.requestId === activityLog.requestId) === index,
    ),
    oldestLoadedMessageId: detailPage.oldestLoadedMessageId,
    hasOlderMessages: detailPage.hasOlderMessages,
    refreshedAt: new Date().toISOString(),
  };
}

function buildShareVisibleText(text: string) {
  return stripHiddenPreviewTags(extractAttachmentPreviewUrls(text).strippedText).trim();
}

function resolvePromptParentQuestionText(request: ChatConversationRequest | null | undefined) {
  return request?.userText?.replace(/\s+/g, ' ').trim() || '';
}

function resolveShareConversationParentRequest(
  request: ChatConversationRequest | null | undefined,
  requestById: ReadonlyMap<string, ChatConversationRequest>,
) {
  const parentRequestId = request?.parentRequestId?.trim() || '';
  return parentRequestId ? requestById.get(parentRequestId) ?? null : null;
}

function resolveShareRequestLineage(
  request: ChatConversationRequest | null | undefined,
  requestById: ReadonlyMap<string, ChatConversationRequest>,
) {
  const directParentRequest = resolveShareConversationParentRequest(request, requestById);

  if (!directParentRequest) {
    return {
      directParentRequest: null,
      topParentRequest: null,
    };
  }

  let currentRequest: ChatConversationRequest | null = directParentRequest;
  let topParentRequest: ChatConversationRequest | null = directParentRequest;
  const visitedRequestIds = new Set<string>();

  while (currentRequest) {
    const currentRequestId = currentRequest.requestId.trim();

    if (!currentRequestId || visitedRequestIds.has(currentRequestId)) {
      break;
    }

    visitedRequestIds.add(currentRequestId);
    topParentRequest = currentRequest;
    currentRequest = resolveShareConversationParentRequest(currentRequest, requestById);
  }

  return {
    directParentRequest,
    topParentRequest,
  };
}

function buildLinkedRoomDraftTitle(source: ChatConversationSummary) {
  const titleBase =
    source.title?.trim()
    || source.requestBadgeLabel?.trim()
    || source.contextLabel?.trim()
    || '연결 작업';
  return `${titleBase} 작업방`;
}

function buildLinkedRoomDraftSeedMessage(source: ChatConversationSummary) {
  const preview =
    source.lastRequestPreview?.trim()
    || source.lastMessagePreview?.trim()
    || source.lastResponsePreview?.trim()
    || '원 세션 내용을 참고해 이어서 처리해 주세요.';
  return `참조 세션을 기준으로 작업을 이어갑니다.\n\n원 요청 요약: ${preview}`;
}

function buildShareRoomSourceGroups(
  rooms: ChatShareRoomSummary[],
  conversationBySessionId: ReadonlyMap<string, ChatConversationSummary>,
) {
  const groupMap = new Map<string, ShareRoomSourceGroup>();

  rooms.forEach((room) => {
    const linkContext = room.linkContext?.kind === 'linked-session' ? room.linkContext : null;
    const linkedConversation = linkContext ? conversationBySessionId.get(linkContext.sourceSessionId) ?? null : null;
    const key = linkContext ? `linked:${linkContext.sourceSessionId}` : `room:${room.sessionId}`;
    const current = groupMap.get(key);
    const title =
      linkContext?.sourceTitle?.trim()
      || linkedConversation?.title?.trim()
      || room.title.trim()
      || '공유 채팅방';
    const requestPreview =
      linkContext?.sourceRequestPreview?.trim()
      || linkedConversation?.lastRequestPreview?.trim()
      || linkedConversation?.lastMessagePreview?.trim()
      || '';
    const chatTypeLabel =
      linkContext?.sourceChatTypeLabel?.trim()
      || linkedConversation?.contextLabel?.trim()
      || room.contextLabel?.trim()
      || '';

    if (current) {
      current.rooms.push(room);
      return;
    }

    groupMap.set(key, {
      key,
      title,
      requestPreview,
      chatTypeLabel,
      sourceSessionId: linkContext?.sourceSessionId ?? null,
      sourceRequestId: linkContext?.sourceRequestId ?? null,
      linkContext,
      rooms: [room],
    });
  });

  return Array.from(groupMap.values()).map((group) => ({
    ...group,
    rooms: [...group.rooms].sort((left, right) => {
      if (left.isDefault !== right.isDefault) {
        return left.isDefault ? -1 : 1;
      }

      if (left.sortOrder !== right.sortOrder) {
        return left.sortOrder - right.sortOrder;
      }

      return left.title.localeCompare(right.title, 'ko');
    }),
  }));
}

function dedupeShareRooms(rooms: ChatShareRoomSummary[]) {
  const dedupedRooms: ChatShareRoomSummary[] = [];
  const knownSessionIds = new Set<string>();

  rooms.forEach((room) => {
    const normalizedSessionId = room.sessionId.trim();

    if (!normalizedSessionId || knownSessionIds.has(normalizedSessionId)) {
      return;
    }

    knownSessionIds.add(normalizedSessionId);
    dedupedRooms.push(room);
  });

  return dedupedRooms;
}

function buildSharePreviewItemsFromText(text: string, shareToken: string) {
  if (!shareToken) {
    return [];
  }

  return extractPreviewItems([
    {
      id: -1,
      author: 'user',
      text,
      timestamp: '',
    },
  ]).map((item) => ({
    ...item,
    url: resolveShareScopedResourceUrl(item.url, shareToken),
  }));
}

function resolveShareScopedResourceUrl(url: string, shareToken: string) {
  const normalizedShareToken = shareToken.trim();
  const normalizedUrl = normalizeChatResourceUrl(String(url ?? '').trim());
  const storedSharePin = getStoredChatShareAccessPin(normalizedShareToken);

  if (!normalizedShareToken || !normalizedUrl || typeof window === 'undefined') {
    return normalizedUrl;
  }

  try {
    const resolvedUrl = new URL(normalizedUrl, window.location.origin);
    let relativePath = '';
    const searchParams = new URLSearchParams(resolvedUrl.search);

    searchParams.delete('token');
    searchParams.delete('clientId');
    searchParams.delete('sharePin');

    if (resolvedUrl.pathname.startsWith(CHAT_API_RESOURCE_ROUTE_PREFIX)) {
      relativePath = resolvedUrl.pathname.slice(CHAT_API_RESOURCE_ROUTE_PREFIX.length);
    } else if (resolvedUrl.pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_PREFIX)) {
      relativePath = resolvedUrl.pathname.slice(CHAT_PUBLIC_DOT_CODEX_PREFIX.length);
    } else if (resolvedUrl.pathname.startsWith(CHAT_PUBLIC_ROUTE_PREFIX)) {
      relativePath = resolvedUrl.pathname.slice(CHAT_PUBLIC_ROUTE_PREFIX.length);
    } else if (resolvedUrl.pathname.startsWith(RESOURCE_MANAGER_PREVIEW_ROUTE_PREFIX)) {
      const resourceRelativePath = decodeURIComponent(
        resolvedUrl.pathname.slice(RESOURCE_MANAGER_PREVIEW_ROUTE_PREFIX.length),
      ).replace(/^\/+/, '');

      if (!resourceRelativePath) {
        return normalizedUrl;
      }

      relativePath = `${RESOURCE_MANAGER_ROOT_PREFIX}${resourceRelativePath}`;
    }

    if (!relativePath) {
      return normalizedUrl;
    }

    const encodedPath = relativePath
      .split('/')
      .filter(Boolean)
      .map((segment) => encodeURIComponent(decodeURIComponent(segment)))
      .join('/');

    if (storedSharePin) {
      searchParams.set('sharePin', storedSharePin);
    }

    const nextSearch = searchParams.toString();
    return `${window.location.origin}/api/chat/shares/${encodeURIComponent(normalizedShareToken)}/resources/${encodedPath}${nextSearch ? `?${nextSearch}` : ''}${resolvedUrl.hash || ''}`;
  } catch {
    return normalizedUrl;
  }
}
function rewritePromptPartForShare(
  prompt: Extract<ChatMessagePart, { type: 'prompt' }>,
  shareToken: string,
): Extract<ChatMessagePart, { type: 'prompt' }> {
  const rewritePreview = <T extends { preview?: { url?: string | null } | null }>(option: T): T => ({
    ...option,
    preview: option.preview
      ? {
          ...option.preview,
          url: option.preview.url ? resolveShareScopedResourceUrl(option.preview.url, shareToken) : option.preview.url ?? null,
        }
      : option.preview ?? null,
  });

  return {
    ...prompt,
    options: prompt.options.map((option) => rewritePreview(option)),
    steps: prompt.steps?.map((step) => ({
      ...step,
      options: step.options.map((option) => rewritePreview(option)),
    })),
  };
}

function isRequestProcessing(status: ChatConversationRequest['status']) {
  return status === 'accepted' || status === 'started';
}

function isRequestInFlight(status: ChatConversationRequest['status']) {
  return status === 'accepted' || status === 'queued' || status === 'started';
}

function isDisconnectedShareRequestNeedingAttention(request: ChatConversationRequest) {
  if (request.hasResponse) {
    return false;
  }

  if (request.status !== 'failed') {
    return false;
  }

  const statusMessage = request.statusMessage?.trim() ?? '';
  return statusMessage === '중단된 오래된 요청';
}

function resolveRequestStatusTag(status: ChatConversationRequest['status']) {
  switch (status) {
    case 'queued':
      return { label: '대기열', color: 'default' as const };
    case 'accepted':
    case 'started':
      return { label: '처리중', color: 'processing' as const };
    case 'completed':
      return { label: '완료', color: 'success' as const };
    case 'failed':
      return { label: '실패', color: 'error' as const };
    case 'cancelled':
      return { label: '취소', color: 'warning' as const };
    case 'removed':
      return { label: '삭제', color: 'default' as const };
    default:
      return { label: status, color: 'default' as const };
  }
}

function isRetriedRequest(request: ChatConversationRequest | null | undefined) {
  return Math.max(0, Number(request?.retryCount ?? 0) || 0) > 0;
}

function hasUnresolvedPromptPart(message: ChatMessage) {
  const promptParts = (message.parts ?? []).filter(
    (part): part is Extract<ChatMessagePart, { type: 'prompt' }> => part.type === 'prompt',
  );

  return promptParts.some((part) => !isPromptResolved(part));
}

function countUnresolvedPromptParts(
  promptParts: Extract<ChatMessagePart, { type: 'prompt' }>[],
) {
  return promptParts.filter((part) => !isPromptResolved(part)).length;
}

function buildSharePromptSearchText(prompt: Extract<ChatMessagePart, { type: 'prompt' }>) {
  return [
    prompt.title,
    prompt.description,
    ...prompt.options.map((option) => `${option.label} ${option.description ?? ''}`.trim()),
    ...(prompt.steps?.flatMap((step) => [
      step.title,
      step.description ?? '',
      ...step.options.map((option) => `${option.label} ${option.description ?? ''}`.trim()),
    ]) ?? []),
  ]
    .map((item) => item.trim())
    .filter(Boolean)
    .join('\n');
}

function hasPendingPromptRequest(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
  promptSubmittedCount = 0,
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  if (request.manualPromptCompletedAt) {
    return false;
  }

  let unresolvedPromptCount = 0;

  relatedMessages.forEach((message) => {
    if (message.author !== 'codex' && message.author !== 'system') {
      return;
    }

    const promptParts = messageRenderPayloadById?.get(message.id)?.promptParts;
    const unresolvedCount = (promptParts ?? (message.parts ?? []).filter(
      (part): part is Extract<ChatMessagePart, { type: 'prompt' }> => part.type === 'prompt',
    )).filter((part) => !isPromptResolved(part)).length;

    unresolvedPromptCount += unresolvedCount;
  });

  if (unresolvedPromptCount === 0) {
    return false;
  }

  return unresolvedPromptCount > Math.max(0, promptSubmittedCount);
}

function hasCodexVisibleResponseMessage(message: ChatMessage, payload?: ShareMessageRenderPayload) {
  if (message.author !== 'codex' && message.author !== 'system') {
    return false;
  }

  const resolvedPayload = payload ?? resolveRenderedMessage(message);
  const { visibleText, promptParts, diffBlocks, previewItems } = resolvedPayload;

  return Boolean(
    promptParts.length > 0 ||
    previewItems.length > 0 ||
    diffBlocks.length > 0 ||
    visibleText.trim(),
  );
}

function hasAnsweredShareRequest(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  if (request.hasResponse) {
    return true;
  }

  return relatedMessages.some((message) => hasCodexVisibleResponseMessage(message, messageRenderPayloadById?.get(message.id)));
}

function hasPendingAttentionVerificationTarget(text: string | null | undefined) {
  const normalized = String(text ?? '').trim();

  if (!normalized) {
    return false;
  }

  if (normalized.length > 720) {
    return true;
  }

  return /```diff[\s\S]*?```|\[\[preview:|\[\[link-card:|\[\[prompt:/i.test(normalized);
}

function hasVerificationTargetShareRequest(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
) {
  if (request.responseMessageId != null) {
    return true;
  }

  if (String(request.responseText ?? '').trim().length > 0) {
    return true;
  }

  if (request.status === 'completed' && request.requestOrigin === 'composer') {
    return relatedMessages.some(
      (message) =>
        (message.author === 'codex' || message.author === 'system')
        && hasPendingAttentionVerificationTarget(message.text),
    );
  }

  return false;
}

function buildShareChildRequestCountMap(requests: ChatConversationRequest[]) {
  const nextMap = new Map<string, number>();

  requests.forEach((request) => {
    const parentRequestId = request.parentRequestId?.trim() || '';
    const requestId = request.requestId.trim();

    if (!parentRequestId || parentRequestId === requestId) {
      return;
    }

    nextMap.set(parentRequestId, (nextMap.get(parentRequestId) ?? 0) + 1);
  });

  return nextMap;
}

function buildSharePromptFollowupCountMap(requests: ChatConversationRequest[]) {
  const nextMap = new Map<string, number>();

  requests.forEach((request) => {
    if (request.requestOrigin !== 'prompt') {
      return;
    }

    const parentRequestId = request.parentRequestId?.trim() || '';

    if (!parentRequestId) {
      return;
    }

    nextMap.set(parentRequestId, (nextMap.get(parentRequestId) ?? 0) + 1);
  });

  return nextMap;
}

function isShareReplyFollowupRequest(request: ChatConversationRequest) {
  return request.requestOrigin === 'composer' && Boolean(request.parentRequestId?.trim());
}

function isPromptFollowupShareRequest(request: ChatConversationRequest) {
  return request.requestOrigin === 'prompt';
}

function hasShareChildFollowupRequest(
  request: ChatConversationRequest,
  childRequestCountByParentId?: Map<string, number>,
) {
  return (childRequestCountByParentId?.get(request.requestId.trim()) ?? 0) > 0;
}

function hasShareManualCompletion(request: ChatConversationRequest) {
  return Boolean(request.manualPromptCompletedAt || request.manualVerificationCompletedAt);
}

function isCompletedHandledShareRequest(
  request: ChatConversationRequest,
  childRequestCountByParentId?: Map<string, number>,
) {
  if (hasShareManualCompletion(request)) {
    return true;
  }

  return hasShareChildFollowupRequest(request, childRequestCountByParentId);
}

function resolveSharePrimaryRequestId(
  request: ChatConversationRequest | null | undefined,
  requestById: ReadonlyMap<string, ChatConversationRequest>,
) {
  let currentRequest = request ?? null;
  const visitedRequestIds = new Set<string>();

  while (currentRequest) {
    const currentRequestId = currentRequest.requestId.trim();

    if (!currentRequestId || visitedRequestIds.has(currentRequestId)) {
      break;
    }

    visitedRequestIds.add(currentRequestId);

    if (currentRequest.requestOrigin !== 'prompt') {
      return currentRequestId;
    }

    const parentRequestId = currentRequest.parentRequestId?.trim() || '';

    if (!parentRequestId) {
      return currentRequestId;
    }

    currentRequest = requestById.get(parentRequestId) ?? null;
  }

  return request?.requestId?.trim() || '';
}

function isUnreadAnsweredShareRequest(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
  childRequestCountByParentId?: Map<string, number>,
  promptFollowupCountByParentId?: Map<string, number>,
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  if (isRequestInFlight(request.status)) {
    return false;
  }

  if (
    hasPendingPromptRequest(
      request,
      relatedMessages,
      promptFollowupCountByParentId?.get(request.requestId.trim()) ?? 0,
      messageRenderPayloadById,
    )
  ) {
    return false;
  }

  if (request.manualPromptCompletedAt) {
    return false;
  }

  if (hasShareChildFollowupRequest(request, childRequestCountByParentId)) {
    return false;
  }

  if (request.manualVerificationCompletedAt) {
    return false;
  }

  if (!hasVerificationTargetShareRequest(request, relatedMessages)) {
    return false;
  }

  return hasAnsweredShareRequest(request, relatedMessages, messageRenderPayloadById);
}

function isPendingCompletionShareRequest(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
  childRequestCountByParentId?: Map<string, number>,
  promptFollowupCountByParentId?: Map<string, number>,
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  if (isCompletedHandledShareRequest(request, childRequestCountByParentId)) {
    return false;
  }

  if (isRequestInFlight(request.status)) {
    return true;
  }

  if (request.status === 'failed') {
    return true;
  }

  if (
    hasPendingPromptRequest(
      request,
      relatedMessages,
      promptFollowupCountByParentId?.get(request.requestId.trim()) ?? 0,
      messageRenderPayloadById,
    )
  ) {
    return true;
  }

  if (isDisconnectedShareRequestNeedingAttention(request)) {
    return true;
  }

  if (request.status === 'cancelled' || request.status === 'removed') {
    return false;
  }

  if (hasVerificationTargetShareRequest(request, relatedMessages)) {
    return hasAnsweredShareRequest(request, relatedMessages, messageRenderPayloadById);
  }

  return request.status === 'completed';
}

function isShareRequestAwaitingManualAction(
  request: ChatConversationRequest,
  relatedMessages: ChatMessage[],
  childRequestCountByParentId?: Map<string, number>,
  promptFollowupCountByParentId?: Map<string, number>,
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  if (isCompletedHandledShareRequest(request, childRequestCountByParentId)) {
    return false;
  }

  if (request.status === 'failed') {
    return true;
  }

  if (
    hasPendingPromptRequest(
      request,
      relatedMessages,
      promptFollowupCountByParentId?.get(request.requestId.trim()) ?? 0,
      messageRenderPayloadById,
    )
  ) {
    return true;
  }

  if (isDisconnectedShareRequestNeedingAttention(request)) {
    return true;
  }

  if (isRequestInFlight(request.status)) {
    return false;
  }

  if (request.status === 'cancelled' || request.status === 'removed') {
    return false;
  }

  if (hasVerificationTargetShareRequest(request, relatedMessages)) {
    return hasAnsweredShareRequest(request, relatedMessages, messageRenderPayloadById);
  }

  return request.status === 'completed';
}

type SharePromptFollowupState = {
  hasInFlight: boolean;
  hasPendingCompletion: boolean;
  hasManualAction: boolean;
  latestChildRequest: ChatConversationRequest;
};

function buildSharePromptFollowupStateByPrimaryRequestId(
  requests: ChatConversationRequest[],
  requestById: ReadonlyMap<string, ChatConversationRequest>,
  requestMessagesById: Map<string, ChatMessage[]>,
  promptFollowupCountByParentId?: Map<string, number>,
  messageRenderPayloadById?: Map<number, ShareMessageRenderPayload>,
) {
  const nextMap = new Map<string, SharePromptFollowupState>();

  requests.forEach((request) => {
    if (!isPromptFollowupShareRequest(request)) {
      return;
    }

    const primaryRequestId = resolveSharePrimaryRequestId(request, requestById);

    if (!primaryRequestId || primaryRequestId === request.requestId.trim()) {
      return;
    }

    const relatedMessages = requestMessagesById.get(request.requestId) ?? [];
    const hasInFlight = isRequestInFlight(request.status);
    const hasPendingCompletion = isPendingCompletionShareRequest(
      request,
      relatedMessages,
      undefined,
      promptFollowupCountByParentId,
      messageRenderPayloadById,
    );
    const hasManualAction = isShareRequestAwaitingManualAction(
      request,
      relatedMessages,
      undefined,
      promptFollowupCountByParentId,
      messageRenderPayloadById,
    );

    if (!hasInFlight && !hasPendingCompletion && !hasManualAction) {
      return;
    }

    const previous = nextMap.get(primaryRequestId);
    const currentUpdatedAt = request.updatedAt?.trim() || request.createdAt;
    const previousUpdatedAt = previous?.latestChildRequest.updatedAt?.trim() || previous?.latestChildRequest.createdAt || '';

    nextMap.set(primaryRequestId, {
      hasInFlight: hasInFlight || previous?.hasInFlight === true,
      hasPendingCompletion: hasPendingCompletion || previous?.hasPendingCompletion === true,
      hasManualAction: hasManualAction || previous?.hasManualAction === true,
      latestChildRequest:
        !previous || currentUpdatedAt.localeCompare(previousUpdatedAt) >= 0
          ? request
          : previous.latestChildRequest,
    });
  });

  return nextMap;
}

function summarizeShareReplyReferenceText(text: string, maxLength = 96) {
  const normalized = stripHiddenPreviewTags(text).replace(/\s+/gu, ' ').trim();

  if (normalized.length <= maxLength) {
    return normalized;
  }

  return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
}

function resolveShareRequestQuestionText(request: ChatConversationRequest | null | undefined) {
  if (!request) {
    return '';
  }

  return buildShareVisibleText(request.userText);
}

function summarizeActivityLogLines(lines: string[]) {
  const normalizedLines = lines.map((line) => line.trim()).filter(Boolean);

  if (normalizedLines.length === 0) {
    return [] as string[];
  }

  const summaries: string[] = [];
  const pushUnique = (label: string) => {
    if (!summaries.includes(label)) {
      summaries.push(label);
    }
  };

  normalizedLines.forEach((line) => {
    const lower = line.toLowerCase();

    if (/(read|search|inspect|context|analysis|analy|조사|확인|정리|검토|조회)/.test(lower)) {
      pushUnique('요청 내용을 확인하고 필요한 정보를 정리하고 있어요.');
    }

    if (/(edit|patch|write|implement|fix|update|modify|반영|수정|작성)/.test(lower)) {
      pushUnique('답변에 반영할 내용을 만들고 있어요.');
    }

    if (/(test|build|verify|check|validate|검증|점검|실행)/.test(lower)) {
      pushUnique('결과를 점검하고 있어요.');
    }

    if (/(file|resource|upload|preview|첨부|파일)/.test(lower)) {
      pushUnique('참고 자료와 첨부 내용을 함께 살펴보고 있어요.');
    }
  });

  if (summaries.length > 0) {
    return summaries.slice(0, 3);
  }

  const latestLine = normalizedLines[normalizedLines.length - 1]
    ?.replace(/[`*_>#-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();

  return latestLine ? [`현재 작업을 진행하고 있어요. ${latestLine}`] : ['현재 작업을 진행하고 있어요.'];
}

function formatElapsedDuration(startedAt: string | null | undefined, nowMs: number) {
  const normalized = startedAt?.trim();

  if (!normalized) {
    return '';
  }

  const startedMs = new Date(normalized).getTime();

  if (Number.isNaN(startedMs)) {
    return '';
  }

  const diffSeconds = Math.max(0, Math.floor((nowMs - startedMs) / 1000));
  const hours = Math.floor(diffSeconds / 3600);
  const minutes = Math.floor((diffSeconds % 3600) / 60);
  const seconds = diffSeconds % 60;

  if (hours > 0) {
    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  }

  return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}

function formatShareRuntimeTimestamp(value: string | null | undefined) {
  const normalized = value?.trim();

  if (!normalized) {
    return '-';
  }

  const parsed = new Date(normalized);

  if (Number.isNaN(parsed.getTime())) {
    return '-';
  }

  return parsed.toLocaleString('ko-KR', {
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  });
}

function resolveShareProcessRequestStatusTag(request: ChatConversationRequest) {
  switch (request.status) {
    case 'accepted':
      return { color: 'blue', label: '접수됨' } as const;
    case 'queued':
      return { color: 'default', label: '대기중' } as const;
    case 'started':
      return { color: 'processing', label: '처리중' } as const;
    case 'completed':
      return { color: 'green', label: '완료' } as const;
    case 'failed':
      return { color: 'red', label: '실패' } as const;
    case 'cancelled':
      return { color: 'gold', label: '취소됨' } as const;
    case 'removed':
    default:
      return { color: 'default', label: '대기취소' } as const;
  }
}

function resolveShareRuntimeTerminalTag(terminalStatus: ChatRuntimeTerminalStatus) {
  switch (terminalStatus) {
    case 'cancelled':
      return { color: 'gold', label: '취소됨' } as const;
    case 'removed':
      return { color: 'default', label: '대기취소' } as const;
    case 'failed':
      return { color: 'red', label: '실패' } as const;
    case 'completed':
    default:
      return { color: 'green', label: '완료' } as const;
  }
}

function resolveShareRuntimeStatusTag(item: ChatRuntimeJobItem) {
  if (item.status === 'running') {
    return { color: 'processing', label: '실행중' } as const;
  }

  return { color: 'default', label: '대기중' } as const;
}

function matchesShareProcessKeywords(lines: string[], pattern: RegExp) {
  return lines.some((line) => pattern.test(line.toLowerCase()));
}

function resolveShareProcessChecklistStepStatus(
  request: ChatConversationRequest,
  enabled: boolean,
  completed: boolean,
): ShareProcessChecklistStep['status'] {
  if (completed) {
    return 'completed';
  }

  if (enabled || request.status === 'started') {
    return 'in_progress';
  }

  return 'pending';
}

function buildShareProcessInspectorPayload(
  request: ChatConversationRequest,
  activityLines: string[],
  nowMs: number,
  runtimeItem?: ChatRuntimeJobItem | (ChatRuntimeJobItem & { terminalStatus?: ChatRuntimeTerminalStatus; lastUpdatedAt?: string }) | null,
): ShareProcessInspectorPayload {
  const normalizedLines = activityLines.map((line) => line.trim()).filter(Boolean);
  const startedAt = runtimeItem?.startedAt ?? runtimeItem?.enqueuedAt ?? request.updatedAt?.trim() ?? request.createdAt;
  const updatedAt = 'lastUpdatedAt' in (runtimeItem ?? {}) ? runtimeItem?.lastUpdatedAt ?? request.updatedAt : request.updatedAt;
  const latestActivityLine = normalizedLines[normalizedLines.length - 1] ?? '';
  const hasAnalysis = matchesShareProcessKeywords(normalizedLines, /(read|search|inspect|context|analysis|analy|조사|확인|정리|검토|조회)/);
  const hasImplementation = matchesShareProcessKeywords(normalizedLines, /(edit|patch|write|implement|fix|update|modify|반영|수정|작성)/);
  const hasVerification = matchesShareProcessKeywords(normalizedLines, /(test|build|verify|check|validate|검증|점검|실행)/);
  const isTerminal = ['completed', 'failed', 'cancelled', 'removed'].includes(request.status);
  const answerSummary = summarizeShareReplyReferenceText(
    request.responseText || request.statusMessage || latestActivityLine || '아직 기록된 응답이 없습니다.',
    120,
  );
  const summary = summarizeShareReplyReferenceText(request.userText || runtimeItem?.summary || '요약 정보 없음', 140);
  const analysisStatus = resolveShareProcessChecklistStepStatus(request, hasAnalysis, hasAnalysis && (hasImplementation || hasVerification || isTerminal));
  const implementationStatus = resolveShareProcessChecklistStepStatus(
    request,
    hasImplementation,
    (hasImplementation && (hasVerification || isTerminal)) || request.hasResponse,
  );
  const verificationStatus = resolveShareProcessChecklistStepStatus(
    request,
    hasVerification || request.hasResponse,
    request.status === 'completed' || request.hasResponse,
  );
  const currentStepLabel =
    verificationStatus === 'in_progress'
      ? '검증/정리'
      : implementationStatus === 'in_progress'
        ? '응답 작성'
        : analysisStatus === 'in_progress'
          ? '요청 분석'
          : request.status === 'queued'
            ? '대기'
            : request.status === 'completed'
              ? '완료'
              : resolveShareProcessRequestStatusTag(request).label;

  return {
    requestId: request.requestId,
    statusTag: resolveShareProcessRequestStatusTag(request),
    summary,
    elapsedLabel: formatElapsedDuration(startedAt, nowMs) || '-',
    startedAtLabel: formatShareRuntimeTimestamp(startedAt),
    updatedAtLabel: formatShareRuntimeTimestamp(updatedAt),
    activityLines: normalizedLines,
    latestActivityLine,
    checklist: [
      {
        key: 'accepted',
        label: '요청 접수',
        status: 'completed',
        note: `${formatShareRuntimeTimestamp(request.createdAt)} 접수`,
      },
      {
        key: 'analysis',
        label: '계획·문맥 확인',
        status: analysisStatus,
        note: hasAnalysis ? '질문과 문맥, 관련 리소스를 확인 중입니다.' : '활동 로그 대기',
      },
      {
        key: 'implementation',
        label: '실행·응답 작성',
        status: implementationStatus,
        note: hasImplementation ? '수정/실행/응답 초안을 진행 중입니다.' : '아직 실행 단계에 도달하지 않았습니다.',
      },
      {
        key: 'verification',
        label: '검증·결과 정리',
        status: verificationStatus,
        note: request.status === 'completed' ? '최종 응답 또는 결과 정리가 끝났습니다.' : hasVerification ? '검증과 결과 정리를 진행 중입니다.' : '검증 단계 대기',
      },
    ],
    narratives: [
      `현재 단계는 ${currentStepLabel}입니다.`,
      latestActivityLine ? `최근 실행 설명: ${latestActivityLine}` : '최근 실행 설명이 아직 기록되지 않았습니다.',
      request.hasResponse ? `현재 응답 요약: ${answerSummary}` : '아직 최종 응답은 기록되지 않았습니다.',
    ],
  };
}

async function createSharePreviewFetchError(response: Response): Promise<SharePreviewFetchError> {
  const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
  let responseMessage = '';

  try {
    if (contentType.includes('application/json')) {
      const payload = (await response.json()) as { message?: string };
      responseMessage = String(payload.message ?? '').trim();
    } else {
      responseMessage = (await response.text()).trim();
    }
  } catch {
    responseMessage = '';
  }

  const statusLabel =
    response.status === 403
      ? '이 문서는 현재 권한으로 열 수 없습니다.'
      : response.status === 404
        ? '이 문서를 찾을 수 없습니다.'
        : response.status === 401
          ? '이 문서를 열기 위한 인증이 필요합니다.'
          : `preview 요청이 실패했습니다. (${response.status})`;
  const detail = responseMessage && responseMessage !== response.statusText ? responseMessage : response.statusText.trim();
  const error = new Error(detail ? `${statusLabel} ${detail}` : statusLabel) as SharePreviewFetchError;
  error.status = response.status;
  return error;
}

function ExpandableMessageText({
  text,
  className,
  collapsedLabel = '더보기',
  expandedLabel = '접기',
  onExpand,
}: {
  text: string;
  className?: string;
  collapsedLabel?: string;
  expandedLabel?: string;
  onExpand?: (() => void) | null;
}) {
  const normalizedText = text.trim();
  const [isExpanded, setIsExpanded] = useState(false);
  const canCollapse = useMemo(() => isCollapsibleText(normalizedText), [normalizedText]);

  useEffect(() => {
    setIsExpanded(false);
  }, [normalizedText]);

  if (!normalizedText) {
    return null;
  }

  return (
    <div className="chat-share-page__collapsible-text">
      <Paragraph
        className={`${className ?? 'chat-share-page__message-body'}${!isExpanded && canCollapse ? ' chat-share-page__message-body--collapsed' : ''}`}
      >
        {normalizedText}
      </Paragraph>
      {canCollapse ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__collapse-toggle"
          onClick={() =>
            setIsExpanded((current) => {
              const nextExpanded = !current;

              if (nextExpanded) {
                onExpand?.();
              }

              return nextExpanded;
            })
          }
        >
          {isExpanded ? expandedLabel : collapsedLabel}
        </Button>
      ) : null}
    </div>
  );
}

function ShareMessageTextBlock({
  tone,
  label,
  text,
  timestamp,
  actions,
}: {
  tone: 'question' | 'answer';
  label: string;
  text: string;
  timestamp?: string | null;
  actions?: ReactNode;
}) {
  return (
    <div className={`chat-share-page__message-tone chat-share-page__message-tone--${tone}`}>
      <div className="chat-share-page__message-tone-head">
        <div className="chat-share-page__message-tone-meta">
          <span className="chat-share-page__message-tone-label">{label}</span>
          {timestamp ? <span className="chat-share-page__message-tone-time">{timestamp}</span> : null}
        </div>
        {actions ? <div className="chat-share-page__message-actions">{actions}</div> : null}
      </div>
      <ExpandableMessageText text={text} />
    </div>
  );
}

function ShareResponseBlock({
  message,
  requestById,
  fallbackRequestId,
  emphasisLabel,
  onSubmitPrompt,
  promptSelections,
  onPromptSelectionChange,
  onPromptSubmitted,
  onCompletePrompt,
  onCompleteVerification,
  isPromptCompletionSaving,
  isPromptManualCompleted,
  isVerificationCompletionSaving,
  isVerificationCompleted,
  hasChildRequest,
  activeReplyRequestId,
  onReplyToResponse,
  shareToken,
  onOpenProgram,
  canUploadAttachments = false,
  onUploadAttachment,
  onSetResponseAnchor,
  onSetPromptAnchor,
  onCopyMessage,
}: {
  message: ChatMessage;
  requestById: Map<string, ChatConversationRequest>;
  fallbackRequestId?: string;
  emphasisLabel?: string;
  onSubmitPrompt: (payload: PromptSubmitPayload & { parentRequestId: string; promptIndex: number; sourceMessageId: number }) => Promise<boolean>;
  promptSelections: Record<string, PendingSharePromptSelection>;
  onPromptSelectionChange: (selectionKey: string, selection: PromptDraftSelection | null) => void;
  onPromptSubmitted: (selectionKey: string, selection: PromptDraftSelection) => void;
  onCompletePrompt?: ((parentRequestId: string) => Promise<void>) | null;
  onCompleteVerification?: ((parentRequestId: string) => Promise<void>) | null;
  isPromptCompletionSaving?: boolean;
  isPromptManualCompleted?: boolean;
  isVerificationCompletionSaving?: boolean;
  isVerificationCompleted?: boolean;
  hasChildRequest?: boolean;
  activeReplyRequestId?: string | null;
  onReplyToResponse?: ((parentRequestId: string) => void) | null;
  shareToken?: string;
  onOpenProgram?: ((target: ShareProgramTarget) => void) | null;
  canUploadAttachments?: boolean;
  onUploadAttachment?: ((file: File) => Promise<ChatComposerAttachment>) | null;
  onSetResponseAnchor?: ((messageId: number, element: HTMLDivElement | null) => void) | null;
  onSetPromptAnchor?: ((messageId: number, promptIndex: number, element: HTMLDivElement | null) => void) | null;
  onCopyMessage?: ((text: string, label: string) => void) | null;
}) {
  const { visibleText, promptParts: rawPromptParts, diffBlocks, previewItems: rawPreviewItems } = useMemo(
    () => extractShareMessageRenderPayload(message),
    [message],
  );
  const promptParts = useMemo(
    () => rawPromptParts.map((part) => (shareToken ? rewritePromptPartForShare(part, shareToken) : part)),
    [rawPromptParts, shareToken],
  );
  const previewItems = useMemo(
    () =>
      shareToken
        ? rawPreviewItems.map((item) => ({
            ...item,
            url: resolveShareScopedResourceUrl(item.url, shareToken),
          }))
        : [],
    [rawPreviewItems, shareToken],
  );
  const parentRequestId = message.clientRequestId?.trim() || fallbackRequestId?.trim() || '';
  const parentRequest = parentRequestId ? requestById.get(parentRequestId) ?? null : null;
  const isParentRequestInFlight = parentRequest ? isRequestInFlight(parentRequest.status) : false;
  const parentQuestionText = useMemo(
    () => resolvePromptParentQuestionText(parentRequestId ? requestById.get(parentRequestId) ?? null : null),
    [parentRequestId, requestById],
  );
  const canCompletePrompt =
    countUnresolvedPromptParts(promptParts) > 0 &&
    !isPromptManualCompleted &&
    !isParentRequestInFlight;
  const hasOpenPromptInResponse = countUnresolvedPromptParts(promptParts) > 0;
  const canCompleteVerification =
    !hasOpenPromptInResponse &&
    Boolean(parentRequestId) &&
    Boolean(onCompleteVerification) &&
    !hasChildRequest &&
    !isVerificationCompleted &&
    !isParentRequestInFlight;
  const canReplyToResponse =
    !hasOpenPromptInResponse &&
    Boolean(parentRequestId) &&
    Boolean(onReplyToResponse) &&
    !isParentRequestInFlight;
  const isReplyTargetActive = canReplyToResponse && activeReplyRequestId?.trim() === parentRequestId;
  const answerActions = (
    <>
      {canCompletePrompt && parentRequestId && onCompletePrompt ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CheckOutlined />}
          loading={isPromptCompletionSaving}
          aria-label="완료 처리"
          title="완료 처리"
          onClick={() => {
            void onCompletePrompt(parentRequestId);
          }}
        />
      ) : null}
      {canCompleteVerification && parentRequestId && onCompleteVerification ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CheckOutlined />}
          loading={isVerificationCompletionSaving}
          aria-label="완료 처리"
          title="완료 처리"
          onClick={() => {
            void onCompleteVerification(parentRequestId);
          }}
        />
      ) : null}
      {canReplyToResponse && parentRequestId && onReplyToResponse ? (
        <Button
          type="text"
          size="small"
          className={`chat-share-page__message-action-button chat-share-page__response-reply-button${
            isReplyTargetActive ? ' chat-share-page__response-reply-button--active' : ''
          }`}
          icon={<SendOutlined />}
          aria-label={isReplyTargetActive ? '답변 참조 중' : '답변하기'}
          title={isReplyTargetActive ? '답변 참조 중' : '답변하기'}
          onClick={() => {
            onReplyToResponse(parentRequestId);
          }}
        />
      ) : null}
      {onCopyMessage ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CopyOutlined />}
          aria-label="답변 복사"
          title="답변 복사"
          onClick={() => onCopyMessage(visibleText, '답변')}
        />
      ) : null}
    </>
  );

  return (
    <div
      className="chat-share-page__response-block"
      ref={(element) => {
        onSetResponseAnchor?.(message.id, element);
      }}
    >
      <div className="chat-share-page__message-headline chat-share-page__message-headline--inline">
        {emphasisLabel ? <Tag>{emphasisLabel}</Tag> : null}
        {!canCompletePrompt && isPromptManualCompleted ? <Tag color="success">prompt 완료 처리됨</Tag> : null}
        {!canCompleteVerification && !hasOpenPromptInResponse && !hasChildRequest && isVerificationCompleted ? <Tag color="success">응답 확인 완료</Tag> : null}
      </div>
      <ShareMessageTextBlock
        tone="answer"
        label="답변"
        text={visibleText}
        timestamp={formatTimeLabel(message.timestamp)}
        actions={answerActions}
      />
      {promptParts.length > 0
        ? promptParts.map((target, promptIndex) => {
            const promptSignature = buildPromptTargetSignature(target);
            const selectionKey = buildSharePromptSelectionKey(parentRequestId, message.id, promptIndex, target.title, promptSignature);
            const draftSelection = promptSelections[selectionKey]?.status === 'draft' ? promptSelections[selectionKey] : null;
            const submittedSelection = promptSelections[selectionKey]?.status === 'submitted' ? promptSelections[selectionKey] : null;

            return (
              <div
                key={`${message.id}-${promptIndex}-${target.title}`}
                ref={(element) => {
                  onSetPromptAnchor?.(message.id, promptIndex, element);
                }}
              >
                <ChatPromptCard
                  target={target}
                  parentQuestionText={parentQuestionText}
                  readOnly={Boolean(isPromptManualCompleted)}
                  draftSelection={draftSelection}
                  submittedSelection={submittedSelection}
                  onSelectionChange={(selection) => onPromptSelectionChange(selectionKey, selection)}
                  onSubmitted={(selection) => onPromptSubmitted(selectionKey, selection)}
                  onSubmit={(payload) => onSubmitPrompt({ ...payload, parentRequestId, promptIndex, sourceMessageId: message.id })}
                  allowAttachments={canUploadAttachments}
                  attachmentAccept={SHARE_ATTACHMENT_ACCEPT}
                  onUploadAttachment={onUploadAttachment}
                />
              </div>
            );
          })
        : null}
      {shareToken && previewItems.length > 0 ? (
        <div className="chat-share-page__resource-list">
          {previewItems.map((item) => (
            <ShareResourcePreviewCard
              key={item.id}
              item={item}
              shareToken={shareToken}
              onOpenProgram={onOpenProgram}
            />
          ))}
        </div>
      ) : null}
      {diffBlocks.length > 0 ? (
        <div className="chat-share-page__resource-list">
          {diffBlocks.map((diffText, index) => (
            <ShareInlinePreviewCard
              key={`${message.id}-diff-${index}`}
              item={{
                id: `${message.id}-diff-${index}`,
                label: diffBlocks.length > 1 ? `diff preview ${index + 1}` : 'diff preview',
                kind: 'diff',
                previewText: diffText,
              }}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
}

function ShareInlinePreviewCard({
  item,
  onPreviewViewed,
}: {
  item: {
    id: string;
    label: string;
    kind: PreviewKind;
    previewText: string;
  };
  onPreviewViewed?: (() => void) | null;
}) {
  const [isExpanded, setIsExpanded] = useState(false);
  const target = useMemo<ChatPreviewTarget>(
    () => ({
      label: item.label,
      url: item.label,
      kind: item.kind,
    }),
    [item.kind, item.label],
  );

  return (
    <section className="chat-share-page__resource-card app-chat-preview-card">
      <div className="app-chat-preview-card__header">
        <div className="app-chat-preview-card__meta">
          <div className="app-chat-preview-card__titles">
            <span className="app-chat-preview-card__label">{item.label}</span>
            <span className="app-chat-preview-card__kind">{item.kind} preview</span>
          </div>
        </div>
        <div className="app-chat-preview-card__actions">
          <Button
            type="text"
            size="small"
            className="app-chat-preview-card__open-link"
            aria-label={isExpanded ? 'diff preview 접기' : 'diff preview 미리보기'}
            onClick={() =>
              setIsExpanded((current) => {
                const nextExpanded = !current;

                if (nextExpanded) {
                  onPreviewViewed?.();
                }

                return nextExpanded;
              })
            }
            icon={<EyeOutlined />}
          />
        </div>
      </div>
      {isExpanded ? (
        <div className="app-chat-preview-card__body chat-share-page__resource-card-body">
          <ChatPreviewBody
            target={target}
            previewText={item.previewText}
            isPreviewLoading={false}
            previewError=""
            renderHtmlAsFrame
          />
        </div>
      ) : null}
    </section>
  );
}

function ShareResourcePreviewCard({
  item,
  shareToken,
  onPreviewViewed,
  onOpenProgram,
}: {
  item: PreviewItem;
  shareToken: string;
  onPreviewViewed?: (() => void) | null;
  onOpenProgram?: ((target: ShareProgramTarget) => void) | null;
}) {
  const [isExpanded, setIsExpanded] = useState(false);
  const [isPreviewOpen, setIsPreviewOpen] = useState(false);
  const [previewText, setPreviewText] = useState('');
  const [previewError, setPreviewError] = useState('');
  const [previewContentType, setPreviewContentType] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const scopedUrl = useMemo(() => resolveShareScopedResourceUrl(item.url, shareToken), [item.url, shareToken]);
  const target = useMemo<ChatPreviewTarget>(
    () => ({
      label: item.label,
      url: scopedUrl,
      kind: item.kind,
    }),
    [item.kind, item.label, scopedUrl],
  );
  const handleOpenProgram = useCallback(() => {
    onOpenProgram?.({
      key: `${item.id}-program`,
      label: item.label,
      url: scopedUrl,
      kind: item.kind,
      meta: `${item.kind} resource`,
    });
  }, [item.id, item.kind, item.label, onOpenProgram, scopedUrl]);

  useEffect(() => {
    if (!isPreviewOpen) {
      return undefined;
    }

    if (item.kind === 'image' || item.kind === 'video' || item.kind === 'pdf' || item.kind === 'file') {
      setPreviewText('');
      setPreviewError('');
      setPreviewContentType('');
      setIsLoading(false);
      return undefined;
    }

    const controller = new AbortController();
    setIsLoading(true);
    setPreviewError('');
    setPreviewContentType('');

    fetch(scopedUrl, {
      cache: 'no-store',
      credentials: 'include',
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          throw await createSharePreviewFetchError(response);
        }

        setPreviewContentType(response.headers.get('content-type') ?? '');
        const text = await response.text();
        setPreviewText(text);
      })
      .catch((error: unknown) => {
        if (controller.signal.aborted) {
          return;
        }

        setPreviewText('');
        setPreviewContentType('');
        setPreviewError(error instanceof Error ? error.message : 'preview를 불러오지 못했습니다.');
      })
      .finally(() => {
        if (!controller.signal.aborted) {
          setIsLoading(false);
        }
      });

    return () => {
      controller.abort();
    };
  }, [isPreviewOpen, item.kind, scopedUrl]);

  return (
    <section className="chat-share-page__resource-card app-chat-preview-card">
      <div className="app-chat-preview-card__header">
        <div className="app-chat-preview-card__meta">
          <div className="app-chat-preview-card__titles">
            <span className="app-chat-preview-card__label">{item.label}</span>
            <span className="app-chat-preview-card__kind">{item.kind} preview</span>
          </div>
        </div>
        <div className="app-chat-preview-card__actions">
          <Button
            type="text"
            size="small"
            className="app-chat-preview-card__open-link"
            aria-label={isPreviewOpen ? `${item.label} 접기` : `${item.label} 미리보기`}
            onClick={() =>
              setIsPreviewOpen((current) => {
                const nextOpen = !current;

                if (nextOpen) {
                  onPreviewViewed?.();
                }

                return nextOpen;
              })
            }
            icon={<EyeOutlined />}
          />
          <Button
            type="text"
            size="small"
            className="app-chat-preview-card__open-link"
            icon={<FullscreenOutlined />}
            aria-label={`${item.label} 확대`}
            onClick={() => {
              onPreviewViewed?.();
              setIsPreviewOpen(true);
              setIsExpanded(true);
            }}
          />
        </div>
      </div>
      {isPreviewOpen ? (
        <div className="app-chat-preview-card__body chat-share-page__resource-card-body">
          <ChatPreviewBody
            target={target}
            previewText={previewText}
            isPreviewLoading={isLoading}
            previewError={previewError}
            previewContentType={previewContentType || undefined}
            maxMarkdownBlocks={5}
            renderHtmlAsFrame
          />
        </div>
      ) : null}
      <FullscreenPreviewModal
        open={isExpanded}
        title={item.label}
        meta={`${item.kind} preview`}
        fillContent
        onMinimize={onOpenProgram ? () => {
          handleOpenProgram();
          setIsExpanded(false);
        } : null}
        onClose={() => setIsExpanded(false)}
      >
        <ChatPreviewBody
          target={target}
          previewText={previewText}
          isPreviewLoading={isLoading}
          previewError={previewError}
          previewContentType={previewContentType || undefined}
          renderHtmlAsFrame
          fullscreen
        />
      </FullscreenPreviewModal>
    </section>
  );
}

function ShareRequestCard({
  request,
  requestById,
  answerText,
  relatedMessages,
  mode,
  onSubmitPrompt,
  promptSelections,
  onPromptSelectionChange,
  onPromptSubmitted,
  onCompletePrompt,
  onCompleteVerification,
  isPromptCompletionSaving,
  isVerificationCompletionSaving,
  isVerificationCompleted,
  hasChildRequest = false,
  activeReplyRequestId,
  onReplyToResponse,
  onCancelDisconnectedRequest,
  onRetryDisconnectedRequest,
  isRequestCancellationSaving = false,
  isRequestRetrySaving = false,
  shareToken,
  onOpenProgram,
  canUploadAttachments = false,
  onUploadAttachment,
  onSetRequestAnchor,
  onSetResponseAnchor,
  onSetPromptAnchor,
  onOpenPreviousQuestion,
  onCopyMessage,
  onCancelActiveRequest,
  isActiveRequestCancellationSaving = false,
  onResubmitRequestDirect,
  isDirectResubmitSaving = false,
  onOpenProcessInspector,
}: {
  request: ChatConversationRequest;
  requestById: Map<string, ChatConversationRequest>;
  answerText: string;
  relatedMessages: ChatMessage[];
  mode: ShareRequestCardMode;
  onSubmitPrompt: (payload: PromptSubmitPayload & { parentRequestId: string; promptIndex: number; sourceMessageId: number }) => Promise<boolean>;
  promptSelections: Record<string, PendingSharePromptSelection>;
  onPromptSelectionChange: (selectionKey: string, selection: PromptDraftSelection | null) => void;
  onPromptSubmitted: (selectionKey: string, selection: PromptDraftSelection) => void;
  onCompletePrompt?: ((parentRequestId: string) => Promise<void>) | null;
  onCompleteVerification?: ((parentRequestId: string) => Promise<void>) | null;
  isPromptCompletionSaving?: boolean;
  isVerificationCompletionSaving?: boolean;
  isVerificationCompleted?: boolean;
  hasChildRequest?: boolean;
  activeReplyRequestId?: string | null;
  onReplyToResponse?: ((parentRequestId: string) => void) | null;
  onCancelDisconnectedRequest?: ((parentRequestId: string) => Promise<void>) | null;
  isRequestCancellationSaving?: boolean;
  onRetryDisconnectedRequest?: ((parentRequestId: string) => Promise<void>) | null;
  isRequestRetrySaving?: boolean;
  shareToken: string;
  onOpenProgram?: ((target: ShareProgramTarget) => void) | null;
  canUploadAttachments?: boolean;
  onUploadAttachment?: ((file: File) => Promise<ChatComposerAttachment>) | null;
  onSetRequestAnchor?: ((requestId: string, element: HTMLElement | null) => void) | null;
  onSetResponseAnchor?: ((messageId: number, element: HTMLDivElement | null) => void) | null;
  onSetPromptAnchor?: ((messageId: number, promptIndex: number, element: HTMLDivElement | null) => void) | null;
  onOpenPreviousQuestion?: ((requestId: string) => void) | null;
  onCopyMessage?: ((text: string, label: string) => void) | null;
  onCancelActiveRequest?: ((requestId: string) => Promise<void>) | null;
  isActiveRequestCancellationSaving?: boolean;
  onResubmitRequestDirect?: ((requestId: string) => Promise<void>) | null;
  isDirectResubmitSaving?: boolean;
  onOpenProcessInspector?: ((requestId: string) => void) | null;
}) {
  const questionText = useMemo(() => buildShareVisibleText(request.userText), [request.userText]);
  const questionPreviewItems = useMemo(
    () => buildSharePreviewItemsFromText(request.userText, shareToken),
    [request.userText, shareToken],
  );
  const { directParentRequest, topParentRequest } = useMemo(
    () => resolveShareRequestLineage(request, requestById),
    [request, requestById],
  );
  const resolvedAnswerText = answerText.trim() || resolveShareRequestFallbackAnswerText(request);
  const shouldRenderQuestion = mode !== 'answer-only';
  const shouldRenderFullAnswer = mode === 'full' || mode === 'answer-only';
  const snapshotRefreshInFlightViewRef = useRef<'initial' | 'full' | null>(null);
  const snapshotMismatchRefreshKeyRef = useRef('');
  const snapshotRetryRefreshKeyRef = useRef('');
  const roomSwitchPerfRunIdRef = useRef('');
  const isRequestStillRunning = isRequestInFlight(request.status);
  const responseMessages = useMemo(
    () => relatedMessages.filter((message) => message.author !== 'user'),
    [relatedMessages],
  );
  const responseRequestById = useMemo(() => new Map([[request.requestId, request]]), [request]);
  const shouldRenderAnswerSummary = shouldRenderFullAnswer && responseMessages.length === 0;
  const canReplyFromSummary =
    Boolean(onReplyToResponse) &&
    mode !== 'question-only' &&
    !isRequestStillRunning;
  const isSummaryReplyActive = canReplyFromSummary && activeReplyRequestId?.trim() === request.requestId;
  const canCompleteSummaryVerification =
    shouldRenderAnswerSummary &&
    Boolean(onCompleteVerification) &&
    !isVerificationCompleted &&
    !isRequestStillRunning;
  const canCancelDisconnectedRequest =
    Boolean(onCancelDisconnectedRequest)
    && !request.hasResponse
    && request.status === 'failed'
    && (request.statusMessage?.trim() ?? '') === '중단된 오래된 요청';
  const canRetryDisconnectedRequest =
    Boolean(onRetryDisconnectedRequest)
    && !request.hasResponse
    && request.status === 'failed'
    && (request.statusMessage?.trim() ?? '') === '중단된 오래된 요청';
  const canCancelActiveRequest =
    Boolean(onCancelActiveRequest)
    && !request.hasResponse
    && isRequestInFlight(request.status);
  const canResubmitDirectRequest =
    Boolean(onResubmitRequestDirect)
    && !request.hasResponse
    && request.status === 'queued';
  const retryCount = Math.max(0, Number(request.retryCount ?? 0) || 0);
  const hasQuestionLineage = Boolean(directParentRequest || topParentRequest);
  const questionActions = (
    <>
      {hasQuestionLineage && onOpenPreviousQuestion ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CommentOutlined />}
          aria-label="부모 질의 보기"
          title="부모 질의 보기"
          onClick={() => {
            onOpenPreviousQuestion(request.requestId);
          }}
        />
      ) : null}
      {canCancelDisconnectedRequest ? (
        <Button
          type="text"
          size="small"
          danger
          className="chat-share-page__message-action-button"
          loading={isRequestCancellationSaving}
          icon={<ShareCloseIcon />}
          aria-label="취소 처리"
          title="취소 처리"
          onClick={() => {
            void onCancelDisconnectedRequest?.(request.requestId);
          }}
        />
      ) : null}
      {canRetryDisconnectedRequest ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          loading={isRequestRetrySaving}
          icon={<ReloadOutlined />}
          aria-label="재처리"
          title="재처리"
          onClick={() => {
            void onRetryDisconnectedRequest?.(request.requestId);
          }}
        />
      ) : null}
      {canCancelActiveRequest ? (
        <Button
          type="text"
          size="small"
          danger
          className="chat-share-page__message-action-button"
          loading={isActiveRequestCancellationSaving}
          icon={<ShareCloseIcon />}
          aria-label="취소"
          title="취소"
          onClick={() => {
            void onCancelActiveRequest?.(request.requestId);
          }}
        />
      ) : null}
      {canResubmitDirectRequest ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          loading={isDirectResubmitSaving}
          icon={<ThunderboltOutlined />}
          aria-label="즉시전송"
          title="즉시전송"
          onClick={() => {
            void onResubmitRequestDirect?.(request.requestId);
          }}
        />
      ) : null}
      {isRequestStillRunning && onOpenProcessInspector ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<EyeOutlined />}
          aria-label="상세 과정 보기"
          title="상세 과정 보기"
          onClick={() => {
            onOpenProcessInspector(request.requestId);
          }}
        />
      ) : null}
      {questionText && onCopyMessage ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CopyOutlined />}
          aria-label="질문 복사"
          title="질문 복사"
          onClick={() => onCopyMessage(questionText, '질문')}
        />
      ) : null}
    </>
  );
  const answerSummaryActions = (
    <>
      {canCompleteSummaryVerification ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CheckOutlined />}
          loading={isVerificationCompletionSaving}
          aria-label="완료 처리"
          title="완료 처리"
          onClick={() => {
            void onCompleteVerification?.(request.requestId);
          }}
        />
      ) : null}
      {canReplyFromSummary ? (
        <Button
          type="text"
          size="small"
          className={`chat-share-page__message-action-button chat-share-page__response-reply-button${
            isSummaryReplyActive ? ' chat-share-page__response-reply-button--active' : ''
          }`}
          icon={<SendOutlined />}
          aria-label={isSummaryReplyActive ? '답변 참조 중' : '답변하기'}
          title={isSummaryReplyActive ? '답변 참조 중' : '답변하기'}
          onClick={() => {
            onReplyToResponse?.(request.requestId);
          }}
        />
      ) : null}
      {resolvedAnswerText && onCopyMessage ? (
        <Button
          type="text"
          size="small"
          className="chat-share-page__message-action-button"
          icon={<CopyOutlined />}
          aria-label="답변 복사"
          title="답변 복사"
          onClick={() => onCopyMessage(resolvedAnswerText, '답변')}
        />
      ) : null}
    </>
  );

  return (
    <section
      id={`chat-share-request-${request.requestId}`}
      className="chat-share-page__request-block"
      ref={(element) => {
        onSetRequestAnchor?.(request.requestId, element);
      }}
    >
      {isRetriedRequest(request) ? (
        <div className="chat-share-page__message-headline chat-share-page__message-headline--inline">
          <Tag color="processing">재처리 {retryCount}회</Tag>
        </div>
      ) : null}
      {shouldRenderQuestion ? (
        <>
          <ShareMessageTextBlock
            tone="question"
            label="질문"
            text={questionText || '-'}
            timestamp={formatTimeLabel(request.createdAt)}
            actions={questionActions}
          />
          {questionPreviewItems.length > 0 ? (
            <div className="chat-share-page__resource-list">
              {questionPreviewItems.map((item) => (
                <ShareResourcePreviewCard
                  key={`request-${request.requestId}-${item.id}`}
                  item={item}
                  shareToken={shareToken}
                  onOpenProgram={onOpenProgram}
                />
              ))}
            </div>
          ) : null}
        </>
      ) : null}
      {shouldRenderFullAnswer ? (
        <>
          {shouldRenderAnswerSummary ? (
            <>
              <div className="chat-share-page__message-divider" aria-hidden="true" />
              {isVerificationCompleted ? (
                <div className="chat-share-page__message-headline chat-share-page__message-headline--inline">
                  <Tag color="success">응답 확인 완료</Tag>
                </div>
              ) : null}
              <ShareMessageTextBlock
                tone="answer"
                label="답변"
                text={resolvedAnswerText}
                timestamp={formatTimeLabel(resolveRequestMessageTimestamp(request))}
                actions={answerSummaryActions}
              />
            </>
          ) : null}

          {responseMessages.length > 0 ? (
            <div className="chat-share-page__bundle-list">
              {responseMessages.map((message) => (
                <ShareResponseBlock
                  key={message.id}
                  message={message}
                  requestById={responseRequestById}
                  fallbackRequestId={request.requestId}
                  onSubmitPrompt={onSubmitPrompt}
                  promptSelections={promptSelections}
                  onPromptSelectionChange={onPromptSelectionChange}
                  onPromptSubmitted={onPromptSubmitted}
                  onCompletePrompt={onCompletePrompt}
                  onCompleteVerification={onCompleteVerification}
                  isPromptCompletionSaving={isPromptCompletionSaving}
                  isPromptManualCompleted={Boolean(request.manualPromptCompletedAt)}
                  isVerificationCompletionSaving={isVerificationCompletionSaving}
                  isVerificationCompleted={isVerificationCompleted}
                  hasChildRequest={hasChildRequest}
                  activeReplyRequestId={activeReplyRequestId}
                  onReplyToResponse={onReplyToResponse}
                  shareToken={shareToken}
                  onOpenProgram={onOpenProgram}
                  canUploadAttachments={canUploadAttachments}
                  onUploadAttachment={onUploadAttachment}
                  onSetResponseAnchor={onSetResponseAnchor}
                  onSetPromptAnchor={onSetPromptAnchor}
                  onCopyMessage={onCopyMessage}
                />
              ))}
            </div>
          ) : null}
        </>
      ) : null}
    </section>
  );
}

export function ChatSharePage() {
  const { message, modal } = App.useApp();
  const appConfig = useAppConfig();
  const { token = '' } = useParams();
  const normalizedToken = token.trim();
  const initialRequestedRoomSessionId =
    typeof window === 'undefined'
      ? ''
      : readShareRoomSessionIdFromLocation() || readStoredShareLastRoomSessionId(normalizedToken);
  const initialCachedSnapshot =
    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>());
  const responseAnchorRefs = useRef(new Map<number, HTMLDivElement>());
  const promptAnchorRefs = useRef(new Map<string, HTMLDivElement>());
  const isInteractingRef = useRef(false);
  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 snapshotMismatchRefreshKeyRef = useRef('');
  const snapshotRetryRefreshKeyRef = useRef('');
  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);
  const programmaticScrollTargetRef = useRef<'top' | 'bottom' | null>(null);
  const lastScrollTopRef = useRef(0);
  const [snapshot, setSnapshot] = useState<ChatShareSnapshot | null>(initialCachedSnapshot);
  const [requestedRoomSessionId, setRequestedRoomSessionId] = useState<string>(initialRequestedRoomSessionId);
  const requestedRoomSessionIdRef = useRef(requestedRoomSessionId);
  const skipNextRequestedRoomRefreshRef = useRef(false);
  const [isLoading, setIsLoading] = useState(() => initialCachedSnapshot == null);
  const [hasLoadedShareData, setHasLoadedShareData] = useState(initialHasCachedSnapshot);
  const [, setIsRefreshing] = useState(false);
  const [isLoadingFullSnapshot, setIsLoadingFullSnapshot] = useState(false);
  const [isLoadingOlderShareHistory, setIsLoadingOlderShareHistory] = useState(false);
  const [isLiveConnected, setIsLiveConnected] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [requiresAccessPin, setRequiresAccessPin] = useState(false);
  const hasSnapshotRef = useRef(false);
  const requiresAccessPinRef = useRef(false);
  const [accessPinInput, setAccessPinInput] = useState('');
  const [accessPinSubmitError, setAccessPinSubmitError] = useState('');
  const [draftText, setDraftText] = useState('');
  const [composerAttachments, setComposerAttachments] = useState<ChatComposerAttachment[]>([]);
  const [isSending, setIsSending] = useState(false);
  const [isUploadingComposerAttachment, setIsUploadingComposerAttachment] = useState(false);
  const [immediateSendPinnedByToken, setImmediateSendPinnedByToken] = useState<Record<string, boolean>>(() =>
    readStoredShareImmediateSendPinnedByToken(),
  );
  const [nowMs, setNowMs] = useState(() => Date.now());
  const [expandMode, setExpandMode] = useState<ShareExpandMode>('pending');
  const [latestRequestId, setLatestRequestId] = useState('');
  const [sharedComposerCodexModel, setSharedComposerCodexModel] = useState(DEFAULT_CODEX_MODEL);
  const [pendingPromptSelections, setPendingPromptSelections] = useState<Record<string, PendingSharePromptSelection>>({});
  const [pendingPromptCompletionRequestIds, setPendingPromptCompletionRequestIds] = useState<string[]>([]);
  const [pendingVerificationCompletionRequestIds, setPendingVerificationCompletionRequestIds] = useState<string[]>([]);
  const [pendingRequestCancellationIds, setPendingRequestCancellationIds] = useState<string[]>([]);
  const [pendingRequestRetryIds, setPendingRequestRetryIds] = useState<string[]>([]);
  const [isShareRoomListVisible, setIsShareRoomListVisible] = useState(false);
  const [shareRoomListLayerStyle, setShareRoomListLayerStyle] = useState<CSSProperties | null>(null);
  const [conversationToolbarStickyTop, setConversationToolbarStickyTop] = useState(52);
  const [isRoomSwitching, setIsRoomSwitching] = useState(false);
  const [replyReferenceRequestId, setReplyReferenceRequestId] = useState('');
  const [previousQuestionModalRequestId, setPreviousQuestionModalRequestId] = useState('');
  const [showScrollToTop, setShowScrollToTop] = useState(false);
  const [showScrollToBottom, setShowScrollToBottom] = useState(false);
  const [isClearingConversation, setIsClearingConversation] = useState(false);
  const [isSearchOpen, setIsSearchOpen] = useState(false);
  const [isTokenUsageOpen, setIsTokenUsageOpen] = useState(false);
  const [isRoomSettingsOpen, setIsRoomSettingsOpen] = useState(false);
  const [isCreateRoomOpen, setIsCreateRoomOpen] = useState(false);
  const [isSavingRoomSettings, setIsSavingRoomSettings] = useState(false);
  const [isCreatingRoom, setIsCreatingRoom] = useState(false);
  const [isLoadingConversationCandidates, setIsLoadingConversationCandidates] = useState(false);
  const [roomSettingsTabKey, setRoomSettingsTabKey] = useState<'chat-type' | 'default-contexts' | 'room-context' | 'notifications' | 'security' | 'runtime'>('chat-type');
  const [editingRoomTitle, setEditingRoomTitle] = useState('');
  const [editingRoomChatTypeId, setEditingRoomChatTypeId] = useState<string | null>(null);
  const [editingRoomDefaultContextIds, setEditingRoomDefaultContextIds] = useState<string[]>([]);
  const [isEditingRoomDefaultContextsDirty, setIsEditingRoomDefaultContextsDirty] = useState(false);
  const [editingRoomCustomContextTitle, setEditingRoomCustomContextTitle] = useState('');
  const [editingRoomCustomContextContent, setEditingRoomCustomContextContent] = useState('');
  const [editingRoomUseAccessPin, setEditingRoomUseAccessPin] = useState(false);
  const [editingRoomAccessPin, setEditingRoomAccessPin] = useState('');
  const [editingRoomAccessPinPromptTtlMinutes, setEditingRoomAccessPinPromptTtlMinutes] = useState<number | null>(null);
  const [editingRoomNotifyOffline, setEditingRoomNotifyOffline] = useState(false);
  const [creatingRoomTitle, setCreatingRoomTitle] = useState('');
  const [creatingRoomChatTypeId, setCreatingRoomChatTypeId] = useState<string | null>(null);
  const [creatingRoomRequestBadgeLabel, setCreatingRoomRequestBadgeLabel] = useState('');
  const [creatingRoomLinkedSessionId, setCreatingRoomLinkedSessionId] = useState('');
  const [creatingRoomSeedMessage, setCreatingRoomSeedMessage] = useState(
    '이 방에서 이어갈 작업 내용을 남겨 주세요. 필요한 파일이나 참고 문맥이 있으면 함께 적어 주세요.',
  );
  const [conversationCandidates, setConversationCandidates] = useState<ChatConversationSummary[]>([]);
  const [originReplyDraftText, setOriginReplyDraftText] = useState('');
  const [originReplyTargetGroupKey, setOriginReplyTargetGroupKey] = useState('');
  const [isOriginReplyModalOpen, setIsOriginReplyModalOpen] = useState(false);
  const [isSubmittingOriginReply, setIsSubmittingOriginReply] = useState(false);
  const [sourceGroupDetailKey, setSourceGroupDetailKey] = useState('');
  const [roomNotificationClientStatus, setRoomNotificationClientStatus] = useState<ShareNotificationClientStatus>(() =>
    buildShareNotificationClientStatus({
      roomEnabled: false,
      appEnabled: appConfig.chat.receiveRoomNotifications,
      permission: getClientNotificationPermission(),
      registrationReady: false,
    }),
  );
  const [shareRuntimeSnapshot, setShareRuntimeSnapshot] = useState<ChatRuntimeSnapshot | null>(null);
  const [isShareRuntimeLoading, setIsShareRuntimeLoading] = useState(false);
  const [pendingShareRuntimeRequestIds, setPendingShareRuntimeRequestIds] = useState<string[]>([]);
  const [activeProcessInspectorRequestId, setActiveProcessInspectorRequestId] = useState('');
  const [processInspectorMode, setProcessInspectorMode] = useState<ShareProcessInspectorMode>('default');
  const [processInspectorExpandedSection, setProcessInspectorExpandedSection] = useState<ShareProcessInspectorExpandedSection>(null);
  const [optimisticShareRooms, setOptimisticShareRooms] = useState<ChatShareRoomSummary[]>([]);
  const [shareRoomPendingCountsBySessionId, setShareRoomPendingCountsBySessionId] = useState<Record<string, ShareRoomPendingCounts>>({});
  const [isLoadingShareRoomPendingCounts, setIsLoadingShareRoomPendingCounts] = useState(false);
  const [isRefreshingRoomNotificationStatus, setIsRefreshingRoomNotificationStatus] = useState(false);
  const [isSendingRoomNotificationTest, setIsSendingRoomNotificationTest] = useState(false);
  const [isDeletingRoom, setIsDeletingRoom] = useState(false);
  const [pendingDeleteRoomSessionId, setPendingDeleteRoomSessionId] = useState('');
  const [shareRoomFilterKeyword, setShareRoomFilterKeyword] = useState('');
  const [searchKeyword, setSearchKeyword] = useState('');
  const [searchPanelMode, setSearchPanelMode] = useState<ShareSearchPanelMode>('all');
  const [selectedAppEnvironment, setSelectedAppEnvironment] = useState<ShareAppEnvironment>(() =>
    typeof window === 'undefined' ? 'preview' : resolveShareAppEnvironmentFromOrigin(window.location.origin),
  );
  const [programTarget, setProgramTarget] = useState<ShareProgramTarget | null>(null);
  const [minimizedPrograms, setMinimizedPrograms] = useState<ShareMinimizedProgramItem[]>([]);
  const [programReloadKey, setProgramReloadKey] = useState(0);
  const shareRoomPendingCountFetchSequenceRef = useRef(0);
  const shareRoomPendingCountRefreshPromiseBySessionIdRef = useRef<Record<string, Promise<void> | null>>({});
  const shareRoomPendingCountRefreshQueuedBySessionIdRef = useRef<Record<string, boolean>>({});
  const conversationHeaderRef = useRef<HTMLDivElement | null>(null);
  const conversationToolbarRef = useRef<HTMLDivElement | null>(null);
  const roomListTriggerButtonRef = useRef<HTMLButtonElement | null>(null);
  const roomListPanelRef = useRef<HTMLElement | null>(null);
  const processInspectorCardRef = useRef<HTMLDivElement | null>(null);
  const processInspectorDragStateRef = useRef<{
    pointerId: number;
    lastX: number;
    lastY: number;
    captureTarget: HTMLDivElement;
  } | null>(null);
  const processInspectorPositionRef = useRef(getDefaultShareProcessInspectorPosition());
  const [processInspectorPosition, setProcessInspectorPosition] = useState(() => processInspectorPositionRef.current);
  const programMinimizedCardRefs = useRef(new Map<string, HTMLDivElement>());
  const programMinimizedDragStateRef = useRef<{
    key: string;
    pointerId: number;
    lastX: number;
    lastY: number;
    captureTarget: HTMLDivElement;
  } | null>(null);
  const programMinimizedActionTrackingRef = useRef<ShareMinimizedProgramActionTracking | null>(null);
  const programMinimizedActionClickIgnoreUntilRef = useRef(0);
  const programMinimizedMovedRef = useRef(false);
  const minimizedProgramsRef = useRef<ShareMinimizedProgramItem[]>([]);
  const minimizedProgramPositionByKeyRef = useRef<Record<string, ShareMinimizedProgramItem['position']>>({});
  const composerRef = useRef<TextAreaRef | null>(null);
  const composerAttachmentInputRef = useRef<HTMLInputElement | null>(null);
  const composerInputShellRef = useRef<HTMLDivElement | null>(null);
  const immediateSendHoldTimerRef = useRef<number | null>(null);
  const suppressImmediateSendClickRef = useRef(false);
  const focusedMobileInputRef = useRef<HTMLElement | null>(null);
  const composerFocusScrollTimerIdsRef = useRef<number[]>([]);
  const [isComposerViewportCompacted, setIsComposerViewportCompacted] = useState(false);
  const [appLaunchUsage, setAppLaunchUsage] = useState<ShareAppLaunchUsageMap>(() => readShareAppLaunchUsage());

  minimizedProgramsRef.current = minimizedPrograms;
  hasSnapshotRef.current = snapshot != null;
  requiresAccessPinRef.current = requiresAccessPin;
  requestedRoomSessionIdRef.current = requestedRoomSessionId;
  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),
    [shareTokenSetting?.allowedAppIds, snapshot?.share.permissions],
  );
  const sharePermissionSet = useMemo(
    () => new Set((snapshot?.share.permissions ?? []).map((item) => item.trim().toLowerCase()).filter(Boolean)),
    [snapshot?.share.permissions],
  );
  const shareRooms = useMemo(() => {
    const snapshotRooms = snapshot?.rooms ?? [];

    if (optimisticShareRooms.length === 0) {
      return dedupeShareRooms(snapshotRooms);
    }

    const nextRooms = [...snapshotRooms];
    const knownSessionIds = new Set(snapshotRooms.map((room) => room.sessionId));

    optimisticShareRooms.forEach((room) => {
      if (!knownSessionIds.has(room.sessionId)) {
        nextRooms.push(room);
      }
    });

    return dedupeShareRooms(nextRooms);
  }, [optimisticShareRooms, snapshot?.rooms]);
  const conversationCandidateBySessionId = useMemo(
    () => new Map(conversationCandidates.map((item) => [item.sessionId, item])),
    [conversationCandidates],
  );
  const activeShareRoomSessionId = snapshot?.activeSessionId?.trim() || snapshot?.share.sessionId?.trim() || '';
  const selectedShareRoomSessionId = requestedRoomSessionId.trim() || activeShareRoomSessionId;
  const activeShareRoom = useMemo(
    () => shareRooms.find((item) => item.sessionId === selectedShareRoomSessionId) ?? null,
    [selectedShareRoomSessionId, shareRooms],
  );
  const hasVisibleSnapshot = snapshot != null;
  const showRoomSwitchingOverlay = isRoomSwitching && hasVisibleSnapshot;
  const showRoomSwitchingSkeleton = isRoomSwitching && !hasVisibleSnapshot;
  const roomSwitchingStatusLabel = activeShareRoom?.title?.trim() || '선택한 채팅방';
  const filteredShareRooms = useMemo(() => {
    const keyword = shareRoomFilterKeyword.trim().toLowerCase();
    if (!keyword) {
      return shareRooms;
    }

    return shareRooms.filter((room) => {
      const searchIndex = [
        room.title,
        room.contextLabel,
        room.requestBadgeLabel,
        room.sessionId,
        room.linkContext?.sourceTitle,
        room.linkContext?.sourceRequestPreview,
        room.linkContext?.sourceChatTypeLabel,
      ]
        .map((value) => value?.trim().toLowerCase() ?? '')
        .filter(Boolean)
        .join(' ');

      return searchIndex.includes(keyword);
    });
  }, [shareRoomFilterKeyword, shareRooms]);
  const filteredShareRoomGroups = useMemo(
    () => buildShareRoomSourceGroups(filteredShareRooms, conversationCandidateBySessionId),
    [conversationCandidateBySessionId, filteredShareRooms],
  );
  const sourceGroupDetail = useMemo(
    () => filteredShareRoomGroups.find((group) => group.key === sourceGroupDetailKey) ?? null,
    [filteredShareRoomGroups, sourceGroupDetailKey],
  );
  const originReplyTargetGroup = useMemo(
    () => filteredShareRoomGroups.find((group) => group.key === originReplyTargetGroupKey) ?? null,
    [filteredShareRoomGroups, originReplyTargetGroupKey],
  );
  const pendingDeleteRoom = useMemo(
    () => shareRooms.find((item) => item.sessionId === pendingDeleteRoomSessionId) ?? null,
    [pendingDeleteRoomSessionId, shareRooms],
  );
  useEffect(() => {
    if (optimisticShareRooms.length === 0 || !snapshot?.rooms?.length) {
      return;
    }

    const snapshotRoomIds = new Set(snapshot.rooms.map((room) => room.sessionId));
    setOptimisticShareRooms((current) => {
      const next = current.filter((room) => !snapshotRoomIds.has(room.sessionId));
      return next.length === current.length ? current : next;
    });
  }, [optimisticShareRooms.length, snapshot?.rooms]);
  const shareRuntimeRunningItems = shareRuntimeSnapshot?.running ?? [];
  const shareRuntimeQueuedItems = shareRuntimeSnapshot?.queued ?? [];
  const shareRuntimeRecentItems = shareRuntimeSnapshot?.recent ?? [];
  const shareRuntimeItemByRequestId = useMemo(
    () =>
      new Map(
        [...shareRuntimeRunningItems, ...shareRuntimeQueuedItems, ...shareRuntimeRecentItems].map((item) => [
          item.requestId.trim(),
          item,
        ]),
      ),
    [shareRuntimeQueuedItems, shareRuntimeRecentItems, shareRuntimeRunningItems],
  );
  const allowedPlayAppEntries = useMemo(
    () => getReadyPlayAppEntries().filter((entry) => shareAllowedAppIdSet.has(entry.id)),
    [shareAllowedAppIdSet],
  );
  const sortedAllowedPlayAppEntries = useMemo(
    () => [...allowedPlayAppEntries].sort((left, right) => (
      compareShareAppLaunchOrder(
        left.id,
        right.id,
        left.usagePriority ?? 0,
        right.usagePriority ?? 0,
        appLaunchUsage,
      )
    )),
    [allowedPlayAppEntries, appLaunchUsage],
  );
  useEffect(() => {
    if (sortedAllowedPlayAppEntries.length === 0) {
      return;
    }

    if (sortedAllowedPlayAppEntries.some((entry) => isPlayAppSupportedInEnvironment(entry, selectedAppEnvironment))) {
      return;
    }

    const nextEnvironment = resolveFirstSupportedShareAppEnvironment(sortedAllowedPlayAppEntries);
    if (nextEnvironment !== selectedAppEnvironment) {
      setSelectedAppEnvironment(nextEnvironment);
    }
  }, [selectedAppEnvironment, sortedAllowedPlayAppEntries]);
  const canManageSharedAppSettings = sharePermissionSet.has('manage') && shareAllowedAppIdSet.has('app-settings');
  const allowedManagementApps = useMemo(
    () => SHARE_MANAGEMENT_APP_OPTIONS.filter((option) => (
      shareAllowedAppIdSet.has(option.value)
      && (option.value !== 'app-settings' || canManageSharedAppSettings)
    )),
    [canManageSharedAppSettings, shareAllowedAppIdSet],
  );
  const currentShareChatTarget = useMemo(
    () => buildShareChatEnvironmentTarget(snapshot?.share.sharePath ?? null, normalizedToken, selectedAppEnvironment),
    [normalizedToken, selectedAppEnvironment, snapshot?.share.sharePath],
  );
  const sortedAllowedManagementApps = useMemo(
    () => [...allowedManagementApps].sort((left, right) => (
      compareShareAppLaunchOrder(left.value, right.value, left.usagePriority, right.usagePriority, appLaunchUsage)
    )),
    [allowedManagementApps, appLaunchUsage],
  );
  const hasSharedRoomSettingsApp = shareAllowedAppIdSet.has('chat-room-settings');
  const hasWorkServerCommandApp = shareAllowedAppIdSet.has('server-command');
  const canManageSharedTokenSetting = sharePermissionSet.has('manage') && shareAllowedAppIdSet.has('token-setting');
  const canManageSharedRoomSettings = sharePermissionSet.has('manage') && hasSharedRoomSettingsApp;
  const canCreateSharedRooms = sharePermissionSet.has('manage');
  const canEditSharedRoomAccessPin = hasSharedRoomSettingsApp;
  const canOpenSharedRoomSettings = hasSharedRoomSettingsApp;
  const isImmediateSendPinned = normalizedToken ? immediateSendPinnedByToken[normalizedToken] === true : false;
  const [shareWorkServerCommand, setShareWorkServerCommand] = useState<ServerCommandItem | null>(null);
  const refreshRoomNotificationStatus = useCallback(async (roomEnabled: boolean) => {
    const permission = getClientNotificationPermission();
    const appEnabled = appConfig.chat.receiveRoomNotifications;
    const fallbackStatus = buildShareNotificationClientStatus({
      roomEnabled,
      appEnabled,
      permission,
      registrationReady: false,
    });

    setRoomNotificationClientStatus(fallbackStatus);

    if (isPreviewRuntime() || !hasSecureOrigin()) {
      return fallbackStatus;
    }

    setIsRefreshingRoomNotificationStatus(true);

    try {
      if (permission !== 'granted') {
        const nextStatus = buildShareNotificationClientStatus({
          roomEnabled,
          appEnabled,
          permission,
          registrationReady: false,
        });
        setRoomNotificationClientStatus(nextStatus);
        return nextStatus;
      }

      const registration = await getPushServiceWorkerRegistration();
      const deviceId = getSavedNotificationDeviceId().trim();

      if (!registration || !deviceId) {
        setRoomNotificationClientStatus(fallbackStatus);
        return fallbackStatus;
      }

      const subscription = await registration.pushManager.getSubscription();
      if (subscription) {
        await syncExistingWebPushSubscriptionRegistration(registration, { deviceId }).catch(() => null);
      }

      const nextStatus = buildShareNotificationClientStatus({
        roomEnabled,
        appEnabled,
        permission,
        registrationReady: Boolean(subscription),
      });
      setRoomNotificationClientStatus(nextStatus);
      return nextStatus;
    } catch {
      setRoomNotificationClientStatus(fallbackStatus);
      return fallbackStatus;
    } finally {
      setIsRefreshingRoomNotificationStatus(false);
    }
  }, [appConfig.chat.receiveRoomNotifications]);
  const ensureRoomNotificationRegistration = useCallback(async () => {
    if (isPreviewRuntime()) {
      throw new Error('미리보기 런타임에서는 알림 등록을 지원하지 않습니다.');
    }

    if (!hasSecureOrigin()) {
      throw new Error('알림은 HTTPS 또는 localhost 환경에서만 사용할 수 있습니다.');
    }

    if (isAppleMobileDevice() && !isStandaloneDisplayMode()) {
      throw new Error('아이폰에서는 홈 화면에 추가한 PWA에서만 웹 푸시를 사용할 수 있습니다.');
    }

    if (
      typeof window === 'undefined'
      || typeof Notification === 'undefined'
      || typeof navigator === 'undefined'
      || !('serviceWorker' in navigator)
      || !('PushManager' in window)
    ) {
      throw new Error('현재 브라우저에서는 웹 푸시를 지원하지 않습니다.');
    }

    let permission = Notification.permission;

    if (permission !== 'granted') {
      permission = await Notification.requestPermission();
    }

    if (permission !== 'granted') {
      throw new Error('브라우저 알림 권한을 허용한 뒤 다시 시도해 주세요.');
    }

    if (!appConfig.chat.receiveRoomNotifications) {
      const savedAppConfig = await saveAppConfigToServer({
        ...appConfig,
        chat: {
          ...appConfig.chat,
          receiveRoomNotifications: true,
        },
      }, {
        shareToken: normalizedToken,
        skipAutomationNotifications: true,
      });
      setStoredAppConfig(savedAppConfig);
    }

    let registration = await getPushServiceWorkerRegistration();

    if (!registration) {
      await waitForDuration(1500);
      registration = await getPushServiceWorkerRegistration();
    }

    if (!registration) {
      throw new Error('알림 서비스워커 준비가 아직 끝나지 않았습니다. 잠시 후 다시 시도해 주세요.');
    }

    const deviceId = getSavedNotificationDeviceId().trim();

    if (!deviceId) {
      throw new Error('현재 기기 알림 식별자를 확인하지 못했습니다.');
    }

    const subscription = await ensureWebPushSubscriptionRegistered(registration, { deviceId });
    const nextStatus = buildShareNotificationClientStatus({
      roomEnabled: true,
      appEnabled: true,
      permission: 'granted',
      registrationReady: Boolean(subscription),
    });
    setRoomNotificationClientStatus(nextStatus);
    return { registration, deviceId, subscription };
  }, [appConfig, normalizedToken]);
  const handleSendRoomNotificationTest = useCallback(async () => {
    if (!snapshot?.conversation.sessionId) {
      message.warning('공유 채팅방 정보를 다시 불러온 뒤 시도해 주세요.');
      return;
    }

    if (isPreviewRuntime()) {
      message.warning('미리보기 런타임에서는 테스트 알림 전송을 지원하지 않습니다.');
      return;
    }

    if (!hasSecureOrigin()) {
      message.warning('테스트 알림은 HTTPS 또는 localhost 환경에서만 확인할 수 있습니다.');
      return;
    }

    if (isAppleMobileDevice() && !isStandaloneDisplayMode()) {
      message.warning('아이폰에서는 홈 화면에 추가한 PWA에서만 웹 푸시 테스트를 확인할 수 있습니다.');
      return;
    }

    if (
      typeof window === 'undefined'
      || typeof Notification === 'undefined'
      || typeof navigator === 'undefined'
      || !('serviceWorker' in navigator)
      || !('PushManager' in window)
    ) {
      message.warning('현재 브라우저에서는 웹 푸시 테스트를 지원하지 않습니다.');
      return;
    }

    setIsSendingRoomNotificationTest(true);

    try {
      let permission = Notification.permission;

      if (permission !== 'granted') {
        permission = await Notification.requestPermission();
      }

      if (permission !== 'granted') {
        throw new Error('브라우저 알림 권한을 허용한 뒤 다시 시도해 주세요.');
      }

      const registration = await getPushServiceWorkerRegistration();

      if (!registration) {
        throw new Error('알림 서비스워커 준비가 아직 끝나지 않았습니다. 잠시 후 다시 시도해 주세요.');
      }

      const deviceId = getSavedNotificationDeviceId().trim();

      if (!deviceId) {
        throw new Error('현재 기기 알림 식별자를 확인하지 못했습니다.');
      }

      await ensureWebPushSubscriptionRegistered(registration, { deviceId });

      const result = await sendClientNotification({
        title: '공유채팅방 테스트 알림',
        body: `${snapshot.conversation.title?.trim() || '공유채팅방'} 알림 테스트입니다.`,
        threadId: `chat:${snapshot.conversation.sessionId}:share-notification-test`,
        targetDeviceIds: [deviceId],
        data: {
          category: 'chat',
          type: 'chat-share-room-test',
          sessionId: snapshot.conversation.sessionId,
          title: snapshot.conversation.title?.trim() || '공유채팅방',
        },
      });

      if (result.web.sentCount > 0) {
        message.success('현재 기기로 테스트 알림 전송을 요청했습니다.');
      } else if (shouldFallbackToLocalNotification(result)) {
        const shown = await showLocalClientNotification({
          title: '공유채팅방 테스트 알림',
          body: `${snapshot.conversation.title?.trim() || '공유채팅방'} 알림 테스트입니다.`,
          threadId: `chat:${snapshot.conversation.sessionId}:share-notification-test-local`,
          data: {
            category: 'chat',
            type: 'chat-share-room-test-local',
            sessionId: snapshot.conversation.sessionId,
          },
        });

        if (!shown) {
          throw new Error(result.web.reason || '테스트 알림 전송 대상을 찾지 못했습니다.');
        }

        message.warning('서버 푸시 전송은 확인되지 않아 현재 브라우저 로컬 알림으로 대체했습니다.');
      } else {
        throw new Error(result.web.reason || '테스트 알림 전송 결과를 확인하지 못했습니다.');
      }

      await refreshRoomNotificationStatus(editingRoomNotifyOffline);
    } catch (error) {
      message.error(error instanceof Error ? error.message : '테스트 알림 전송에 실패했습니다.');
      await refreshRoomNotificationStatus(editingRoomNotifyOffline);
    } finally {
      setIsSendingRoomNotificationTest(false);
    }
  }, [editingRoomNotifyOffline, message, refreshRoomNotificationStatus, snapshot?.conversation.sessionId, snapshot?.conversation.title]);
  useLayoutEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const sharePathname = `${window.location.pathname}${window.location.search}${window.location.hash}`;
    const activeInstallAppEntry = findReadyPlayAppEntryById(programTarget?.appId);
    const activeInstallAppId = activeInstallAppEntry?.id ?? '';
    const manifestObjectUrl = activeInstallAppEntry
      ? createInstallManifestObjectUrl({
          startPath: buildSharePlayAppInstallPath(activeInstallAppId, normalizedToken),
          scope: '/play/apps',
          name: activeInstallAppEntry.name,
          shortName: activeInstallAppEntry.name,
          description: `${activeInstallAppEntry.name} 앱을 홈 화면에서 바로 엽니다.`,
          themeColor: resolveSharePlayAppInstallThemeColor(activeInstallAppId),
          backgroundColor: '#eff5ff',
        })
      : createChatShareManifestHref(sharePathname, snapshot?.conversation.title);
    const restoreManifest = activeInstallAppEntry
      ? swapInstallDocumentMetadata({
          manifestHref: manifestObjectUrl,
          title: activeInstallAppEntry.name,
          themeColor: resolveSharePlayAppInstallThemeColor(activeInstallAppId),
        })
      : swapManifestForChatShare(manifestObjectUrl, snapshot?.conversation.title);

    return () => {
      restoreManifest();
      if (manifestObjectUrl.startsWith('blob:')) {
        window.URL.revokeObjectURL(manifestObjectUrl);
      }
    };
  }, [normalizedToken, programTarget?.appId, snapshot?.conversation.title]);
  useEffect(() => {
    if (!hasWorkServerCommandApp) {
      setShareWorkServerCommand(null);
      return;
    }

    let cancelled = false;

    void fetchServerCommands({ shareToken: normalizedToken })
      .then((items) => {
        if (cancelled) {
          return;
        }

        setShareWorkServerCommand(items.find((item) => item.key === 'work-server') ?? null);
      })
      .catch((error) => {
        console.error('failed to load shared work-server command status', error);

        if (!cancelled) {
          setShareWorkServerCommand(null);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [hasWorkServerCommandApp, normalizedToken]);

  const setRequestAnchorRef = useCallback((requestId: string, element: HTMLElement | null) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedRequestId) {
      return;
    }

    if (!element) {
      requestAnchorRefs.current.delete(normalizedRequestId);
      return;
    }

    requestAnchorRefs.current.set(normalizedRequestId, element);
  }, []);
  const setResponseAnchorRef = useCallback((messageId: number, element: HTMLDivElement | null) => {
    if (!Number.isFinite(messageId)) {
      return;
    }

    if (!element) {
      responseAnchorRefs.current.delete(messageId);
      return;
    }

    responseAnchorRefs.current.set(messageId, element);
  }, []);
  const setPromptAnchorRef = useCallback((messageId: number, promptIndex: number, element: HTMLDivElement | null) => {
    if (!Number.isFinite(messageId) || !Number.isFinite(promptIndex)) {
      return;
    }

    const anchorKey = buildSharePromptAnchorKey(messageId, promptIndex);

    if (!element) {
      promptAnchorRefs.current.delete(anchorKey);
      return;
    }

    promptAnchorRefs.current.set(anchorKey, element);
  }, []);
  const { chatTypes, isLoading: isChatTypesLoading, errorMessage: chatTypesErrorMessage } = useChatTypeRegistry();
  const {
    defaultContexts,
    chatTypeDefaults,
    roomContexts,
    setStore: setChatContextSettingsStore,
    isLoading: isChatContextSettingsLoading,
    errorMessage: chatContextSettingsErrorMessage,
  } = useChatContextSettingsRegistry();
  const enabledChatTypes = useMemo(() => chatTypes.filter((item) => item.enabled), [chatTypes]);
  const enabledDefaultContexts = useMemo(() => defaultContexts.filter((item) => item.enabled), [defaultContexts]);
  const currentSharedChatTypeId = useMemo(() => {
    const requestChatTypeIds = [
      snapshot?.conversation.chatTypeId,
      snapshot?.conversation.lastChatTypeId,
      snapshot?.targetRequest.chatTypeId,
      ...(snapshot?.requests ?? []).map((item) => item.chatTypeId),
    ];

    return requestChatTypeIds.find((item) => String(item ?? '').trim())?.trim() ?? null;
  }, [snapshot?.conversation.chatTypeId, snapshot?.conversation.lastChatTypeId, snapshot?.requests, snapshot?.targetRequest.chatTypeId]);
  const activeRoomContextSettings = useMemo(
    () => resolveChatRoomContextSettings(roomContexts, snapshot?.conversation.sessionId ?? null),
    [roomContexts, snapshot?.conversation.sessionId],
  );
  const sortedRequests = useMemo(
    () => [...(snapshot?.requests ?? [])].sort(compareShareConversationRequests),
    [snapshot],
  );
  const isServerCommandDrawerMobile = typeof window !== 'undefined' ? window.innerWidth <= 768 : false;
  const openSharedRoomSettings = useCallback(() => {
    if (!snapshot?.conversation.sessionId) {
      return;
    }

    const nextChatTypeId = currentSharedChatTypeId ?? enabledChatTypes[0]?.id ?? null;
    const fallbackRoomTitle =
      snapshot.conversation.title?.trim()
      || snapshot.targetRequest.userText?.trim()
      || snapshot.requests?.[0]?.userText?.trim()
      || '-';
    setEditingRoomTitle(fallbackRoomTitle);
    setEditingRoomChatTypeId(nextChatTypeId);
    setEditingRoomDefaultContextIds(resolveShareRoomDefaultContextIds(activeRoomContextSettings, chatTypeDefaults, nextChatTypeId));
    setIsEditingRoomDefaultContextsDirty(false);
    setEditingRoomCustomContextTitle(activeRoomContextSettings?.customContextTitle ?? '');
    setEditingRoomCustomContextContent(activeRoomContextSettings?.customContextContent ?? '');
    setEditingRoomNotifyOffline(snapshot.conversation.notifyOffline === true);
    setEditingRoomUseAccessPin(snapshot.share.hasAccessPin === true);
    setEditingRoomAccessPin('');
    setEditingRoomAccessPinPromptTtlMinutes(resolveAccessPinPromptTtlMinutes(snapshot.share.accessPinPromptTtlMinutes));
    setRoomSettingsTabKey('chat-type');
    setIsRoomSettingsOpen(true);
  }, [
    activeRoomContextSettings?.customContextContent,
    activeRoomContextSettings?.customContextTitle,
    activeRoomContextSettings?.defaultContextIds,
    chatTypeDefaults,
    currentSharedChatTypeId,
    enabledChatTypes,
    snapshot?.conversation.notifyOffline,
    snapshot?.conversation.title,
    snapshot?.requests,
    snapshot?.share.accessPinPromptTtlMinutes,
    snapshot?.share.hasAccessPin,
    snapshot?.conversation.sessionId,
    snapshot?.targetRequest.userText,
  ]);
  const openCreateRoomDialog = useCallback(() => {
    const nextChatTypeId = currentSharedChatTypeId ?? enabledChatTypes[0]?.id ?? null;
    const nextChatTypeName = enabledChatTypes.find((item) => item.id === nextChatTypeId)?.name ?? '';

    setCreatingRoomTitle(nextChatTypeName ? `${nextChatTypeName} 작업방` : '새 공유 채팅방');
    setCreatingRoomChatTypeId(nextChatTypeId);
    setCreatingRoomRequestBadgeLabel('');
    setCreatingRoomLinkedSessionId('');
    setCreatingRoomSeedMessage('이 방에서 이어갈 작업 내용을 남겨 주세요. 필요한 파일이나 참고 문맥이 있으면 함께 적어 주세요.');
    setIsCreateRoomOpen(true);
    setIsLoadingConversationCandidates(true);
    void fetchChatConversations()
      .then((items) => {
        const nextItems = items.filter((item) => item.sessionId !== snapshot?.conversation.sessionId);
        setConversationCandidates(nextItems);
      })
      .catch((error) => {
        console.error('failed to load share room candidates', error);
        setConversationCandidates([]);
      })
      .finally(() => {
        setIsLoadingConversationCandidates(false);
      });
  }, [currentSharedChatTypeId, enabledChatTypes, snapshot?.conversation.sessionId]);
  const refreshShareRuntime = useCallback(async (options?: { silent?: boolean }) => {
    if (!normalizedToken || !selectedShareRoomSessionId) {
      setShareRuntimeSnapshot(null);
      return false;
    }

    if (!options?.silent) {
      setIsShareRuntimeLoading(true);
    }

    try {
      const nextSnapshot = await fetchChatShareRuntimeSnapshot(normalizedToken, {
        sessionId: selectedShareRoomSessionId,
      });
      setShareRuntimeSnapshot(nextSnapshot);
      return true;
    } catch (error) {
      if (!options?.silent) {
        message.error(error instanceof Error ? error.message : '처리중 세션 상태를 불러오지 못했습니다.');
      }
      return false;
    } finally {
      setIsShareRuntimeLoading(false);
    }
  }, [message, normalizedToken, selectedShareRoomSessionId]);
  useEffect(() => {
    if (!isRoomSettingsOpen) {
      return;
    }

    void refreshRoomNotificationStatus(editingRoomNotifyOffline);
  }, [appConfig.chat.receiveRoomNotifications, editingRoomNotifyOffline, isRoomSettingsOpen, refreshRoomNotificationStatus]);
  useEffect(() => {
    if (!isRoomSettingsOpen || roomSettingsTabKey !== 'runtime') {
      return;
    }

    void refreshShareRuntime();
  }, [isRoomSettingsOpen, refreshShareRuntime, roomSettingsTabKey]);
  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) {
      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;
      }
    }

    const abortController = new AbortController();
    snapshotRefreshAbortControllerRef.current = abortController;
    snapshotRefreshInFlightRoomSessionIdRef.current = requestedSessionId;
    snapshotRefreshInFlightViewRef.current = requestView;

    if (options?.initialLoad) {
      if (!(preserveVisibleSnapshot && hasSnapshotRef.current)) {
        setIsLoading(true);
      }
    } else if (!silent) {
      setIsRefreshing(true);
    }

    const refreshTaskPromise = (async () => {
      try {
        recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-fetch-start`);
        const nextSnapshot = await fetchChatShareSnapshot(normalizedToken, {
          sharePin: options?.sharePin,
          sessionId: requestedSessionId || undefined,
          signal: abortController.signal,
          view: requestView,
        });
        recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-fetch-end`, nextSnapshot);
        const matchedRequestedRoom = doesShareSnapshotMatchRequestedRoom(nextSnapshot, requestedSessionId);

        const shouldApplyImmediately =
          !isInteractingRef.current
          || initialLoad
          || requiresAccessPinRef.current
          || !hasSnapshotRef.current
          || isRoomSwitchingRef.current;

        if (!shouldApplyImmediately) {
          deferredSnapshotRef.current = nextSnapshot;
        } else {
          pendingSnapshotPerfStageRef.current = currentPerfRunId
            ? { runId: currentPerfRunId, stage: requestView }
            : null;
          if (!initialLoad && requestView === 'full' && hasSnapshotRef.current) {
            startTransition(() => {
              setSnapshot(nextSnapshot);
            });
          } else {
            setSnapshot(nextSnapshot);
          }
          deferredSnapshotRef.current = null;
        }
        writeStoredShareRoomSnapshot(normalizedToken, nextSnapshot);
        if (nextSnapshot.share.hasAccessPin) {
          const resolvedPin = normalizeAccessPinInput(options?.sharePin ?? '');
          const persistedPin = resolvedPin || getStoredChatShareAccessPin(normalizedToken);
          if (persistedPin) {
            setStoredChatShareAccessPin(normalizedToken, persistedPin, {
              expiresAt: nextSnapshot.share.accessPinSessionExpiresAt,
              ttlMinutes: nextSnapshot.share.accessPinPromptTtlMinutes,
            });
          }
        } else {
          setStoredChatShareAccessPin(normalizedToken, null);
        }

        if (matchedRequestedRoom) {
          recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-matched-requested-room`, nextSnapshot);
          snapshotMismatchRefreshKeyRef.current = '';
          snapshotRetryRefreshKeyRef.current = '';
          setIsRoomSwitching(false);
        } else if (requestedSessionId) {
          pendingSilentRefreshRef.current = true;
          snapshotRetryRefreshKeyRef.current = `${requestedSessionId}:${requestView}:retry`;
        }

        setErrorMessage('');
        setRequiresAccessPin(false);
        setAccessPinSubmitError('');

        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);
          setSnapshot(null);
          setRequiresAccessPin(true);

          if (error.code === 'share_pin_invalid') {
            setAccessPinInput('');
            setAccessPinSubmitError(error.message);
          } else {
            setAccessPinSubmitError('');
          }
        }

        if (!silent) {
          setErrorMessage(error instanceof Error ? error.message : '공유 화면을 불러오지 못했습니다.');
        }
        setIsRoomSwitching(false);
        return false;
      } finally {
        if (snapshotRefreshAbortControllerRef.current === abortController) {
          snapshotRefreshAbortControllerRef.current = null;
        }
        if (snapshotRefreshPromiseRef.current === refreshTaskPromise) {
          snapshotRefreshPromiseRef.current = null;
        }
        if (snapshotRefreshInFlightRoomSessionIdRef.current === requestedSessionId) {
          snapshotRefreshInFlightRoomSessionIdRef.current = '';
          snapshotRefreshInFlightViewRef.current = null;
        }
        setHasLoadedShareData(true);
        setIsLoading(false);
        setIsRefreshing(false);

        if (pendingSilentRefreshRef.current) {
          const retryKey = `${requestedSessionId}:${requestView}:retry`;
          if (snapshotRetryRefreshKeyRef.current === retryKey) {
            pendingSilentRefreshRef.current = false;
            snapshotRetryRefreshKeyRef.current = `${requestedSessionId}:${requestView}:done`;
            return;
          }

          snapshotRetryRefreshKeyRef.current = retryKey;
          pendingSilentRefreshRef.current = false;
          window.setTimeout(() => {
            void refreshSnapshot({ silent: true, view: 'full' });
          }, 0);
        }
      }
    })();

    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;
    }

    setIsLoadingFullSnapshot(true);
    setIsLoadingOlderShareHistory(true);

    try {
      const scrollContainer = scrollContainerRef.current;
      const previousScrollHeight = scrollContainer?.scrollHeight ?? 0;
      const previousScrollTop = scrollContainer?.scrollTop ?? 0;
      const detailPage = await fetchChatShareConversationDetail(normalizedToken, {
        sessionId: snapshot.conversation.sessionId,
        beforeMessageId: snapshot.oldestLoadedMessageId,
        limit: SHARE_HISTORY_PAGE_SIZE,
        sharePin: getStoredChatShareAccessPin(normalizedToken) || undefined,
      });

      setSnapshot((current) => (current ? mergeChatShareSnapshotHistory(current, detailPage) : current));

      window.requestAnimationFrame(() => {
        if (!scrollContainer) {
          return;
        }

        const nextScrollHeight = scrollContainer.scrollHeight;
        scrollContainer.scrollTop = Math.max(0, previousScrollTop + (nextScrollHeight - previousScrollHeight));
      });
    } catch (error) {
      message.error(error instanceof Error ? error.message : '이전 대화를 불러오지 못했습니다.');
    } finally {
      setIsLoadingFullSnapshot(false);
      setIsLoadingOlderShareHistory(false);
    }
  }, [message, normalizedToken, snapshot]);
  const handleDeleteShareRoom = useCallback(async (room: ChatShareRoomSummary) => {
    if (!normalizedToken || !room.sessionId || isDeletingRoom) {
      return;
    }

    if (!canDeleteShareRoom(room, shareRooms)) {
      message.warning(room.isDefault ? '기본 채팅방은 삭제할 수 없습니다.' : '마지막 채팅방은 삭제할 수 없습니다.');
      return;
    }

    setIsDeletingRoom(true);

    try {
      const result = await deleteChatShareRoom(normalizedToken, room.sessionId);
      const fallbackSessionId =
        result.nextRoomSessionId
        || shareRooms.find((item) => item.sessionId !== room.sessionId)?.sessionId
        || '';

      setOptimisticShareRooms((current) => current.filter((item) => item.sessionId !== room.sessionId));
      setSnapshot((current) => {
        if (!current) {
          return current;
        }

        const nextRooms = current.rooms.filter((item) => item.sessionId !== room.sessionId);
        const nextActiveSessionId =
          current.activeSessionId === room.sessionId
            ? (result.nextRoomSessionId || nextRooms.find((item) => item.isDefault)?.sessionId || nextRooms[0]?.sessionId || '')
            : current.activeSessionId;

        return {
          ...current,
          rooms: nextRooms,
          activeSessionId: nextActiveSessionId,
        };
      });
      setPendingDeleteRoomSessionId('');
      setSwipedRoomSessionId('');
      removeStoredShareRoomSnapshot(normalizedToken, room.sessionId);
      requestedRoomSessionIdRef.current =
        requestedRoomSessionIdRef.current === room.sessionId ? fallbackSessionId : requestedRoomSessionIdRef.current;
      setRequestedRoomSessionId((current) => (current === room.sessionId ? fallbackSessionId : current));

      if (selectedShareRoomSessionId === room.sessionId) {
        setDraftText('');
        setComposerAttachments([]);
        setReplyReferenceRequestId('');
        setLatestRequestId('');
        setExpandMode('pending');
      }

      await refreshSnapshot({ silent: true });
      message.success(`"${room.title}" 채팅방을 삭제했습니다.`);
    } catch (error) {
      message.error(error instanceof Error ? error.message : '공유 채팅방 삭제 중 오류가 발생했습니다.');
    } finally {
      setIsDeletingRoom(false);
    }
  }, [
    isDeletingRoom,
    message,
    normalizedToken,
    refreshSnapshot,
    selectedShareRoomSessionId,
    shareRooms,
  ]);
  const handleCancelShareRuntimeRequest = useCallback((item: ChatRuntimeJobItem) => {
    const actionLabel = item.status === 'queued' ? '대기 요청' : '실행 중 요청';

    modal.confirm({
      title: `${actionLabel}을 취소할까요?`,
      content: item.summary || '요약 정보가 없는 요청입니다.',
      okText: '취소 실행',
      cancelText: '닫기',
      autoFocusButton: 'cancel',
      okButtonProps: { danger: true },
      onOk: async () => {
        setPendingShareRuntimeRequestIds((current) => [...current, item.requestId]);

        try {
          const action = await cancelChatShareRuntimeRequest(normalizedToken, {
            requestId: item.requestId,
            sessionId: activeShareRoomSessionId,
          });
          message.success(action === 'removed' ? '대기 요청을 취소했습니다.' : '실행 중 요청 취소를 요청했습니다.');
          await Promise.all([
            refreshShareRuntime({ silent: true }),
            refreshSnapshot({ silent: true }),
          ]);
        } catch (error) {
          message.error(error instanceof Error ? error.message : '처리중 세션 취소에 실패했습니다.');
        } finally {
          setPendingShareRuntimeRequestIds((current) => current.filter((requestId) => requestId !== item.requestId));
        }
      },
    });
  }, [activeShareRoomSessionId, message, modal, normalizedToken, refreshShareRuntime, refreshSnapshot]);
  const handleCopyShareMessageText = useCallback(async (text: string, label: string) => {
    const normalizedText = text.trim();

    if (!normalizedText) {
      message.warning(`복사할 ${label} 내용이 없습니다.`);
      return;
    }

    try {
      await copyTextToClipboard(normalizedText);
      message.success(`${label}을 복사했습니다.`);
    } catch (error) {
      console.error(`failed to copy ${label} text`, error);
      message.error(`${label} 복사에 실패했습니다.`);
    }
  }, [message]);
  const handleCancelActiveShareRequest = useCallback(async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedToken || !normalizedRequestId || pendingShareRuntimeRequestIds.includes(normalizedRequestId)) {
      return;
    }

    const targetRequest = sortedRequests.find((item) => item.requestId === normalizedRequestId) ?? null;

    if (!targetRequest || targetRequest.hasResponse || !isRequestInFlight(targetRequest.status)) {
      return;
    }

    const actionLabel = targetRequest.status === 'queued' ? '대기 요청' : '실행 중 요청';
    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: `${actionLabel}을 취소할까요?`,
        content: targetRequest.userText.trim() || targetRequest.statusMessage?.trim() || '요약 정보가 없는 요청입니다.',
        okText: '취소 실행',
        cancelText: '닫기',
        autoFocusButton: 'cancel',
        okButtonProps: { danger: true },
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setPendingShareRuntimeRequestIds((current) => Array.from(new Set([...current, normalizedRequestId])));

    try {
      const action = await cancelChatShareRuntimeRequest(normalizedToken, {
        requestId: normalizedRequestId,
        sessionId: activeShareRoomSessionId,
      });
      message.success(action === 'removed' ? '대기 요청을 취소했습니다.' : '실행 중 요청 취소를 요청했습니다.');
      await Promise.all([
        refreshShareRuntime({ silent: true }),
        refreshSnapshot({ silent: true }),
      ]);
    } catch (error) {
      message.error(error instanceof Error ? error.message : '처리 중 요청 취소에 실패했습니다.');
    } finally {
      setPendingShareRuntimeRequestIds((current) => current.filter((item) => item !== normalizedRequestId));
    }
  }, [activeShareRoomSessionId, message, modal, normalizedToken, pendingShareRuntimeRequestIds, refreshShareRuntime, refreshSnapshot, sortedRequests, sharedComposerCodexModel]);
  const handleResubmitQueuedRequestDirect = useCallback(async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedToken || !normalizedRequestId || isSending || pendingShareRuntimeRequestIds.includes(normalizedRequestId)) {
      return;
    }

    const targetRequest = sortedRequests.find((item) => item.requestId === normalizedRequestId) ?? null;

    if (!targetRequest || targetRequest.hasResponse || targetRequest.status !== 'queued') {
      return;
    }

    const outgoingText = buildOutgoingShareMessageText(targetRequest.userText, []).trim();

    if (!outgoingText) {
      message.warning('즉시 전송할 질문 내용이 없습니다.');
      return;
    }

    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: '대기 요청을 즉시전송할까요?',
        content: '기존 대기 요청은 취소하고 같은 내용을 즉시전송으로 다시 보냅니다.',
        okText: '즉시전송',
        cancelText: '닫기',
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setPendingShareRuntimeRequestIds((current) => Array.from(new Set([...current, normalizedRequestId])));
    setIsSending(true);

    try {
      await cancelChatShareRuntimeRequest(normalizedToken, {
        requestId: normalizedRequestId,
        sessionId: activeShareRoomSessionId,
      });
      await submitChatShareMessage(normalizedToken, outgoingText, {
        sessionId: activeShareRoomSessionId,
        mode: 'direct',
        parentRequestId: targetRequest.parentRequestId?.trim() || '',
        codexModel: sharedComposerCodexModel,
      });
      message.success('대기 요청을 취소하고 즉시전송했습니다.');
      await Promise.all([
        refreshShareRuntime({ silent: true }),
        refreshSnapshot({ silent: true }),
      ]);
    } catch (error) {
      if (isShareSendDelayError(error)) {
        message.warning('즉시전송 후 응답 확인이 지연되고 있습니다. 연결 복구 시 최신 내용을 다시 불러옵니다.');
      } else {
        message.error(error instanceof Error ? error.message : '즉시전송 처리에 실패했습니다.');
      }
    } finally {
      setIsSending(false);
      setPendingShareRuntimeRequestIds((current) => current.filter((item) => item !== normalizedRequestId));
    }
  }, [activeShareRoomSessionId, isSending, message, modal, normalizedToken, pendingShareRuntimeRequestIds, refreshShareRuntime, refreshSnapshot, sharedComposerCodexModel, sortedRequests]);
  const handleSaveSharedRoomSettings = useCallback(async () => {
    if (!snapshot?.conversation.sessionId) {
      setIsRoomSettingsOpen(false);
      return;
    }

    const nextChatType = enabledChatTypes.find((item) => item.id === editingRoomChatTypeId) ?? null;

    if (canManageSharedRoomSettings && !nextChatType) {
      message.warning('채팅유형을 먼저 선택하세요.');
      return;
    }

    if (!canManageSharedRoomSettings && !canEditSharedRoomAccessPin) {
      setIsRoomSettingsOpen(false);
      return;
    }

    const normalizedDefaultContextIds = Array.from(
      new Set(
        editingRoomDefaultContextIds
          .map((item) => item.trim())
          .filter((item) => enabledDefaultContexts.some((context) => context.id === item)),
      ),
    );
    const inheritedDefaultContextIds = resolveChatTypeDefaultContextIds(chatTypeDefaults, nextChatType?.id ?? null);
    const nextCustomContextTitle = editingRoomCustomContextTitle.trim();
    const nextCustomContextContent = editingRoomCustomContextContent.trim();
    const shouldPersistRoomDefaultContextIds = !areStringListsEqual(normalizedDefaultContextIds, inheritedDefaultContextIds);
    const shouldPersistRoomCustomContext = Boolean(nextCustomContextTitle || nextCustomContextContent);
    const currentRoomDefaultContextIds = activeRoomContextSettings?.defaultContextIds ?? [];
    const currentRoomCustomContextTitle = activeRoomContextSettings?.customContextTitle?.trim() ?? '';
    const currentRoomCustomContextContent = activeRoomContextSettings?.customContextContent?.trim() ?? '';
    const currentRoomCodexParticipants = activeRoomContextSettings?.codexParticipants ?? [];
    const shouldSaveRoomContextSettings =
      canManageSharedRoomSettings
      && (
        !areStringListsEqual(normalizedDefaultContextIds, currentRoomDefaultContextIds)
        || nextCustomContextTitle !== currentRoomCustomContextTitle
        || nextCustomContextContent !== currentRoomCustomContextContent
      );
    const normalizedRoomTitle = editingRoomTitle.trim();
    const normalizedAccessPin = editingRoomAccessPin.trim();
    const currentHasAccessPin = snapshot?.share.hasAccessPin === true;
    const currentAccessPinPromptTtlMinutes = resolveAccessPinPromptTtlMinutes(snapshot?.share.accessPinPromptTtlMinutes);
    const shouldSaveConversationSettings =
      canManageSharedRoomSettings
      && Boolean(nextChatType)
      && (
        normalizedRoomTitle !== (snapshot?.conversation.title?.trim() || '')
        || !(snapshot?.conversation.title?.trim())
        || currentSharedChatTypeId !== nextChatType.id
        || snapshot.conversation.notifyOffline !== editingRoomNotifyOffline
      );

    if (canManageSharedRoomSettings && !normalizedRoomTitle) {
      message.warning('채팅방 이름을 입력하세요.');
      return;
    }

    if (canEditSharedRoomAccessPin && editingRoomUseAccessPin && !currentHasAccessPin && !normalizedAccessPin) {
      message.warning('비밀번호를 새로 켜려면 숫자 4자리를 입력하세요.');
      return;
    }

    if (canEditSharedRoomAccessPin && normalizedAccessPin && !/^\d{4}$/u.test(normalizedAccessPin)) {
      message.warning('비밀번호는 숫자 4자리로 입력하세요.');
      return;
    }

    setIsSavingRoomSettings(true);

    try {
      if (
        shouldSaveConversationSettings
        && editingRoomNotifyOffline
        && (
          snapshot.conversation.notifyOffline !== editingRoomNotifyOffline
          || !appConfig.chat.receiveRoomNotifications
          || roomNotificationClientStatus.tone !== 'success'
        )
      ) {
        await ensureRoomNotificationRegistration();
      }

      if (shouldSaveRoomContextSettings) {
        const shouldKeepRoomContextRecord =
          shouldPersistRoomDefaultContextIds
          || shouldPersistRoomCustomContext
          || currentRoomCodexParticipants.length > 0;
        const nextRoomContexts = shouldKeepRoomContextRecord
          ? upsertChatRoomContextSettings(roomContexts, {
              sessionId: snapshot.conversation.sessionId,
              defaultContextIds: normalizedDefaultContextIds,
              customContextTitle: nextCustomContextTitle,
              customContextContent: nextCustomContextContent,
              codexParticipants: currentRoomCodexParticipants,
            })
          : roomContexts.filter((item) => item.sessionId !== snapshot.conversation.sessionId);

        await setChatContextSettingsStore({
          defaultContexts,
          chatTypeDefaults,
          roomContexts: nextRoomContexts,
        });
      }

      const shouldSaveAccessPinSettings =
        canEditSharedRoomAccessPin
        && (
          (!editingRoomUseAccessPin && currentHasAccessPin)
          || (editingRoomUseAccessPin && !currentHasAccessPin)
          || Boolean(normalizedAccessPin)
          || (editingRoomUseAccessPin && currentHasAccessPin && editingRoomAccessPinPromptTtlMinutes !== currentAccessPinPromptTtlMinutes)
        );

      const nextAccessPinUpdate =
        canEditSharedRoomAccessPin
          ? !editingRoomUseAccessPin
            ? currentHasAccessPin
              ? null
              : undefined
            : normalizedAccessPin
              ? normalizedAccessPin
              : undefined
          : undefined;

      if (shouldSaveAccessPinSettings || shouldSaveConversationSettings) {
        const roomSecurity = await saveChatShareRoomSettings(normalizedToken, {
          sessionId: snapshot.conversation.sessionId,
          accessPin: nextAccessPinUpdate,
          accessPinPromptTtlMinutes: editingRoomUseAccessPin ? editingRoomAccessPinPromptTtlMinutes : null,
          chatTypeId: shouldSaveConversationSettings ? nextChatType?.id ?? null : undefined,
          chatTypeLabel: shouldSaveConversationSettings ? nextChatType?.name ?? null : undefined,
          title: shouldSaveConversationSettings ? normalizedRoomTitle : undefined,
          notifyOffline: shouldSaveConversationSettings ? editingRoomNotifyOffline : undefined,
        });

        if (nextAccessPinUpdate === null) {
          setAccessPinInput('');
        }

        if (!roomSecurity.hasAccessPin) {
          setAccessPinSubmitError('');
        }

        setSnapshot((previous) =>
          previous
            ? {
                ...previous,
                share: {
                  ...previous.share,
                  hasAccessPin: roomSecurity.hasAccessPin,
                  accessPinPromptTtlMinutes: roomSecurity.accessPinPromptTtlMinutes,
                },
                conversation: roomSecurity.conversation
                  ? {
                      ...previous.conversation,
                      ...roomSecurity.conversation,
                      title: roomSecurity.conversation.title ?? previous.conversation.title,
                    }
                  : previous.conversation,
              }
            : previous,
        );
      }

      message.success(canManageSharedRoomSettings ? '공유 채팅방 설정을 저장했습니다.' : '공유 비밀번호를 저장했습니다.');
      setIsEditingRoomDefaultContextsDirty(false);
      setIsRoomSettingsOpen(false);
      void refreshSnapshot({ silent: true });
    } catch (error) {
      message.error(error instanceof Error ? error.message : '공유 채팅방 설정 저장에 실패했습니다.');
    } finally {
      setIsSavingRoomSettings(false);
    }
  }, [
    chatTypeDefaults,
    canEditSharedRoomAccessPin,
    canManageSharedRoomSettings,
    defaultContexts,
    editingRoomChatTypeId,
    editingRoomTitle,
    editingRoomAccessPin,
    editingRoomAccessPinPromptTtlMinutes,
    editingRoomCustomContextContent,
    editingRoomCustomContextTitle,
    editingRoomDefaultContextIds,
    editingRoomNotifyOffline,
    editingRoomUseAccessPin,
    enabledChatTypes,
    enabledDefaultContexts,
    ensureRoomNotificationRegistration,
    currentSharedChatTypeId,
    message,
    normalizedToken,
    roomNotificationClientStatus.tone,
    refreshSnapshot,
    activeRoomContextSettings?.codexParticipants,
    activeRoomContextSettings?.customContextContent,
    activeRoomContextSettings?.customContextTitle,
    activeRoomContextSettings?.defaultContextIds,
    roomContexts,
    setChatContextSettingsStore,
    snapshot?.conversation.notifyOffline,
    snapshot?.conversation.sessionId,
    snapshot?.share.accessPinPromptTtlMinutes,
    snapshot?.share.hasAccessPin,
  ]);
  useEffect(() => {
    if (minimizedPrograms.length === 0) {
      programMinimizedDragStateRef.current = null;
      programMinimizedMovedRef.current = false;
      return;
    }

    const clampPosition = (key: string, position: { x: number; y: number }) => {
      const cardElement = programMinimizedCardRefs.current.get(key) ?? null;
      const cardWidth = cardElement?.offsetWidth ?? PROGRAM_MINIMIZED_DEFAULT_WIDTH;
      const cardHeight = cardElement?.offsetHeight ?? PROGRAM_MINIMIZED_DEFAULT_HEIGHT;
      const maxX = Math.max(PROGRAM_MINIMIZED_VIEWPORT_PADDING, window.innerWidth - cardWidth - PROGRAM_MINIMIZED_VIEWPORT_PADDING);
      const maxY = Math.max(PROGRAM_MINIMIZED_VIEWPORT_PADDING, window.innerHeight - cardHeight - PROGRAM_MINIMIZED_VIEWPORT_PADDING);

      return {
        x: clampProgramMinimizedValue(position.x, PROGRAM_MINIMIZED_VIEWPORT_PADDING, maxX),
        y: clampProgramMinimizedValue(position.y, PROGRAM_MINIMIZED_VIEWPORT_PADDING, maxY),
      };
    };

    const syncPosition = (key: string, position: { x: number; y: number }) => {
      const nextPosition = clampPosition(key, position);
      minimizedProgramPositionByKeyRef.current[key] = nextPosition;
      setMinimizedPrograms((current) => current.map((item) => (
        item.target.key === key
          ? {
              ...item,
              position: nextPosition,
            }
          : item
      )));
    };

    const handleResize = () => {
      setMinimizedPrograms((current) => current.map((item) => {
        const nextPosition = clampPosition(item.target.key, item.position);
        minimizedProgramPositionByKeyRef.current[item.target.key] = nextPosition;
        return {
          ...item,
          position: nextPosition,
        };
      }));
    };

    const handlePointerMove = (event: PointerEvent) => {
      const dragState = programMinimizedDragStateRef.current;
      const actionTracking = programMinimizedActionTrackingRef.current;

      if (actionTracking && actionTracking.pointerId === event.pointerId) {
        const deltaX = Math.abs(event.clientX - actionTracking.startX);
        const deltaY = Math.abs(event.clientY - actionTracking.startY);

        if (Math.max(deltaX, deltaY) > SHARE_MINIMIZED_ACTION_TAP_TOLERANCE_PX) {
          actionTracking.cancelled = true;
          if (actionTracking.captureTarget.hasPointerCapture(actionTracking.pointerId)) {
            actionTracking.captureTarget.releasePointerCapture(actionTracking.pointerId);
          }
        }
      }

      if (!dragState || dragState.pointerId !== event.pointerId) {
        return;
      }

      const deltaX = event.clientX - dragState.lastX;
      const deltaY = event.clientY - dragState.lastY;

      if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) {
        programMinimizedMovedRef.current = true;
      }

      dragState.lastX = event.clientX;
      dragState.lastY = event.clientY;

      const currentItem = minimizedProgramsRef.current.find((item) => item.target.key === dragState.key);

      if (!currentItem) {
        return;
      }

      syncPosition(dragState.key, {
        x: currentItem.position.x + deltaX,
        y: currentItem.position.y + deltaY,
      });
    };

    const finishPointerDrag = (event: PointerEvent) => {
      const dragState = programMinimizedDragStateRef.current;
      const actionTracking = programMinimizedActionTrackingRef.current;

      if (!dragState || dragState.pointerId !== event.pointerId) {
        if (actionTracking?.pointerId === event.pointerId) {
          clearProgramMinimizedActionTracking();
        }
        return;
      }

      if (dragState.captureTarget.hasPointerCapture(event.pointerId)) {
        dragState.captureTarget.releasePointerCapture(event.pointerId);
      }

      programMinimizedDragStateRef.current = null;
      if (actionTracking?.pointerId === event.pointerId) {
        clearProgramMinimizedActionTracking();
      }
    };

    window.addEventListener('resize', handleResize);
    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', finishPointerDrag);
    window.addEventListener('pointercancel', finishPointerDrag);
    handleResize();

    return () => {
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', finishPointerDrag);
      window.removeEventListener('pointercancel', finishPointerDrag);
      clearProgramMinimizedActionTracking();
    };
  }, [minimizedPrograms.length]);

  useLayoutEffect(() => {
    const headerElement = conversationHeaderRef.current;

    if (!headerElement) {
      return undefined;
    }

    const updateStickyOffset = () => {
      const headerHeight = headerElement.getBoundingClientRect().height;
      const nextStickyTop = Math.max(48, Math.ceil(headerHeight) + 8);

      setConversationToolbarStickyTop((current) => (current === nextStickyTop ? current : nextStickyTop));
    };

    const resizeObserver =
      typeof ResizeObserver === 'undefined'
        ? null
        : new ResizeObserver(() => {
            updateStickyOffset();
          });

    updateStickyOffset();
    window.addEventListener('resize', updateStickyOffset);
    resizeObserver?.observe(headerElement);

    return () => {
      window.removeEventListener('resize', updateStickyOffset);
      resizeObserver?.disconnect();
    };
  }, []);

  useLayoutEffect(() => {
    if (!isShareRoomListVisible) {
      setShareRoomListLayerStyle(null);
      return;
    }

    const updateLayerPosition = () => {
      const triggerRect = roomListTriggerButtonRef.current?.getBoundingClientRect() ?? null;
      const headerRect = conversationHeaderRef.current?.getBoundingClientRect() ?? null;
      const toolbarRect = conversationToolbarRef.current?.getBoundingClientRect() ?? null;
      const anchorRect = triggerRect ?? headerRect ?? toolbarRect;

      if (!anchorRect) {
        return;
      }

      const viewportPadding = 8;
      const availableWidth = Math.max(280, window.innerWidth - (viewportPadding * 2));
      const preferredWidth = toolbarRect
        ? Math.min(Math.max(toolbarRect.width, 280), 420)
        : headerRect
          ? Math.min(Math.max(headerRect.width, 280), 420)
        : Math.min(360, availableWidth);
      const width = Math.min(preferredWidth, availableWidth);
      const preferredRight = triggerRect?.right ?? toolbarRect?.right ?? headerRect?.right ?? (viewportPadding + width);
      const minLeft = viewportPadding;
      const maxLeft = Math.max(viewportPadding, window.innerWidth - viewportPadding - width);
      const left = Math.min(Math.max(preferredRight - width, minLeft), maxLeft);
      const top = anchorRect.bottom + 8;
      const maxHeight = Math.max(220, window.innerHeight - top - viewportPadding);

      setShareRoomListLayerStyle({
        top: `${Math.round(top)}px`,
        left: `${Math.round(left)}px`,
        width: `${Math.round(width)}px`,
        maxHeight: `${Math.round(maxHeight)}px`,
      });
    };

    const handleEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        setIsShareRoomListVisible(false);
      }
    };

    const handleOutsidePointerDown = (event: PointerEvent) => {
      const target = event.target;
      if (!(target instanceof Node)) {
        return;
      }

      if (roomListPanelRef.current?.contains(target) || roomListTriggerButtonRef.current?.contains(target)) {
        return;
      }

      setIsShareRoomListVisible(false);
    };

    const handleViewportChange = () => {
      updateLayerPosition();
    };

    updateLayerPosition();
    window.addEventListener('resize', handleViewportChange);
    window.addEventListener('scroll', handleViewportChange, true);
    document.addEventListener('pointerdown', handleOutsidePointerDown, true);
    document.addEventListener('keydown', handleEscape);

    return () => {
      window.removeEventListener('resize', handleViewportChange);
      window.removeEventListener('scroll', handleViewportChange, true);
      document.removeEventListener('pointerdown', handleOutsidePointerDown, true);
      document.removeEventListener('keydown', handleEscape);
    };
  }, [isShareRoomListVisible]);

  const handleProcessInspectorPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
    if (event.pointerType === 'mouse' && event.button !== 0) {
      return;
    }

    if (isShareInteractivePointerTarget(event.target)) {
      return;
    }

    const captureTarget = event.currentTarget;
    processInspectorDragStateRef.current = {
      pointerId: event.pointerId,
      lastX: event.clientX,
      lastY: event.clientY,
      captureTarget,
    };
    captureTarget.setPointerCapture(event.pointerId);
    event.preventDefault();
  }, []);

  const openProcessInspector = useCallback(async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedRequestId) {
      return;
    }

    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('');
    setProcessInspectorExpandedSection(null);
  }, []);

  const handleToggleProcessInspectorSummary = useCallback(() => {
    setProcessInspectorExpandedSection((current) => (current === 'summary' ? null : 'summary'));
  }, []);

  const handleToggleProcessInspectorChecklist = useCallback(() => {
    setProcessInspectorExpandedSection((current) => (current === 'checklist' ? null : 'checklist'));
  }, []);

  const handleToggleProcessInspectorNarratives = useCallback(() => {
    setProcessInspectorExpandedSection((current) => (current === 'narratives' ? null : 'narratives'));
  }, []);
  const handleToggleProcessInspectorLog = useCallback(() => {
    setProcessInspectorExpandedSection((current) => (current === 'log' ? null : 'log'));
  }, []);

  const handleProgramMinimizedPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
    if (event.pointerType === 'mouse' && event.button !== 0) {
      return;
    }

    const targetKey = event.currentTarget.dataset.programKey?.trim() ?? '';

    if (!targetKey) {
      return;
    }

    const captureTarget = event.currentTarget;
    programMinimizedDragStateRef.current = {
      key: targetKey,
      pointerId: event.pointerId,
      lastX: event.clientX,
      lastY: event.clientY,
      captureTarget,
    };
    programMinimizedMovedRef.current = false;
    captureTarget.setPointerCapture(event.pointerId);
  }, []);

  const clearProgramMinimizedDragState = useCallback(() => {
    const dragState = programMinimizedDragStateRef.current;

    if (!dragState) {
      programMinimizedMovedRef.current = false;
      return;
    }

    if (dragState.captureTarget.hasPointerCapture(dragState.pointerId)) {
      dragState.captureTarget.releasePointerCapture(dragState.pointerId);
    }

    programMinimizedDragStateRef.current = null;
    programMinimizedMovedRef.current = false;
  }, []);

  const clearProgramMinimizedActionTracking = useCallback(() => {
    const actionTracking = programMinimizedActionTrackingRef.current;

    if (actionTracking?.captureTarget.hasPointerCapture(actionTracking.pointerId)) {
      actionTracking.captureTarget.releasePointerCapture(actionTracking.pointerId);
    }

    programMinimizedActionTrackingRef.current = null;
  }, []);

  const handleProgramMinimizedActionPointerDown = useCallback((event: ReactPointerEvent<HTMLElement>, action: ShareMinimizedProgramAction, targetKey: string) => {
    if (event.pointerType === 'mouse' && event.button !== 0) {
      return;
    }

    const normalizedTargetKey = targetKey.trim();
    if (!normalizedTargetKey) {
      return;
    }

    clearProgramMinimizedDragState();
    clearProgramMinimizedActionTracking();
    programMinimizedActionClickIgnoreUntilRef.current = 0;
    event.preventDefault();
    event.stopPropagation();
    event.currentTarget.setPointerCapture(event.pointerId);
    programMinimizedActionTrackingRef.current = {
      key: normalizedTargetKey,
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      action,
      captureTarget: event.currentTarget,
      cancelled: false,
    };
  }, [clearProgramMinimizedDragState, clearProgramMinimizedActionTracking]);

  const runProgramMinimizedActionAfterPointerCycle = useCallback((action: () => void) => {
    if (typeof window === 'undefined') {
      action();
      return;
    }

    window.requestAnimationFrame(() => {
      action();
    });
  }, []);

  const handleRestoreProgram = useCallback((targetKey: string) => {
    let restoredTarget: ShareProgramTarget | null = null;

    setMinimizedPrograms((current) => current.filter((item) => {
      if (item.target.key === targetKey) {
        restoredTarget = item.target;
        return false;
      }

      return true;
    }));

    if (restoredTarget) {
      setProgramReloadKey(0);
      if (!canLaunchShareProgram(restoredTarget.appId)) {
        message.warning('이 공유 링크에서는 허용되지 않은 앱입니다.');
        return;
      }

      setProgramTarget(restoredTarget);
    }
  }, [canLaunchShareProgram, message]);

  const handleReloadPage = useCallback(() => {
    if (typeof window === 'undefined') {
      return;
    }

    void (async () => {
      await resetNonAuthClientState();
      await forceReloadApp();
    })();
  }, []);

  const handleReloadProgram = useCallback(() => {
    setProgramReloadKey((current) => current + 1);
  }, []);

  useEffect(() => {
    setAccessPinInput('');
    setAccessPinSubmitError('');
    setRequiresAccessPin(false);
  }, [normalizedToken]);

  const handleCloseMinimizedProgram = useCallback((targetKey: string) => {
    setMinimizedPrograms((current) => current.filter((item) => item.target.key !== targetKey));
  }, []);

  const runProgramMinimizedAction = useCallback((targetKey: string, action: ShareMinimizedProgramAction) => {
    if (action === 'restore') {
      runProgramMinimizedActionAfterPointerCycle(() => {
        handleRestoreProgram(targetKey);
      });
      return;
    }

    runProgramMinimizedActionAfterPointerCycle(() => {
      handleCloseMinimizedProgram(targetKey);
    });
  }, [handleRestoreProgram, handleCloseMinimizedProgram, runProgramMinimizedActionAfterPointerCycle]);

  const handleProgramMinimizedActionPointerUp = useCallback((event: ReactPointerEvent<HTMLElement>, action: ShareMinimizedProgramAction, targetKey: string) => {
    const normalizedTargetKey = targetKey.trim();
    const tracking = programMinimizedActionTrackingRef.current;

    if (
      !tracking
      || tracking.pointerId !== event.pointerId
      || tracking.key !== normalizedTargetKey
      || tracking.action !== action
      || tracking.cancelled
    ) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    const now = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    programMinimizedActionClickIgnoreUntilRef.current = now + 500;
    clearProgramMinimizedActionTracking();
    runProgramMinimizedAction(normalizedTargetKey, action);
  }, [runProgramMinimizedAction]);

  const handleProgramMinimizedActionClick = useCallback((event: ReactMouseEvent<HTMLElement>, action: ShareMinimizedProgramAction, targetKey: string) => {
    if (programMinimizedActionTrackingRef.current) {
      return;
    }

    const now = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    if (programMinimizedActionClickIgnoreUntilRef.current > now) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    runProgramMinimizedAction(targetKey.trim(), action);
  }, [runProgramMinimizedAction]);

  const handleProgramMinimizedActionKeyDown = useCallback((event: KeyboardEvent<HTMLElement>, action: ShareMinimizedProgramAction, targetKey: string) => {
    if (event.key !== 'Enter' && event.key !== ' ') {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    runProgramMinimizedAction(targetKey.trim(), action);
  }, [runProgramMinimizedAction]);

  const minimizedProgramCards = minimizedPrograms.map((item, index) => (
    <div
      key={item.target.key}
      ref={(element) => {
        if (element) {
          programMinimizedCardRefs.current.set(item.target.key, element);
          return;
        }

        programMinimizedCardRefs.current.delete(item.target.key);
      }}
      className="chat-share-page__program-minimized"
      style={{
        transform: `translate3d(${item.position.x}px, ${item.position.y}px, 0)`,
        zIndex: SHARE_PROGRAM_MINIMIZED_Z_INDEX + index,
      }}
    >
      <div
        className="chat-share-page__program-minimized-drag"
        data-program-key={item.target.key}
        onPointerDown={handleProgramMinimizedPointerDown}
      >
        <span className="chat-share-page__program-minimized-drag-grip" aria-hidden="true" />
        <span className="chat-share-page__program-minimized-title">{item.target.label}</span>
      </div>
      <div className="chat-share-page__program-minimized-actions">
        <Button
          type="primary"
          size="small"
          icon={<AppstoreOutlined />}
          className="chat-share-page__program-minimized-button"
          onPointerDown={(event: ReactPointerEvent<HTMLElement>) => {
            handleProgramMinimizedActionPointerDown(event, 'restore', item.target.key);
          }}
          onPointerUp={(event: ReactPointerEvent<HTMLElement>) => {
            handleProgramMinimizedActionPointerUp(event, 'restore', item.target.key);
          }}
          onClick={(event: ReactMouseEvent<HTMLElement>) => {
            handleProgramMinimizedActionClick(event, 'restore', item.target.key);
          }}
        >
          열기
        </Button>
        <Button
          type="text"
          size="small"
          className="chat-share-page__program-minimized-icon chat-share-page__program-minimized-close"
          icon={<ShareCloseIcon />}
          aria-label="프로그램 닫기"
          onPointerDown={(event: ReactPointerEvent<HTMLElement>) => {
            handleProgramMinimizedActionPointerDown(event, 'close', item.target.key);
          }}
          onPointerUp={(event: ReactPointerEvent<HTMLElement>) => {
            handleProgramMinimizedActionPointerUp(event, 'close', item.target.key);
          }}
          onClick={(event: ReactMouseEvent<HTMLElement>) => {
            handleProgramMinimizedActionClick(event, 'close', item.target.key);
          }}
        />
      </div>
    </div>
  ));
  const syncScrollJumpVisibility = useCallback(() => {
    const scrollContainer = scrollContainerRef.current;

    if (!scrollContainer) {
      setShowScrollToTop(false);
      setShowScrollToBottom(false);
      return;
    }

    const topDistance = scrollContainer.scrollTop;
    const bottomDistance = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
    const maxScrollDistance = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight);
    const isAtTop = topDistance <= SCROLL_JUMP_HIDE_THRESHOLD;
    const isAtBottom = bottomDistance <= SCROLL_JUMP_HIDE_THRESHOLD;
    const scrollDelta = topDistance - lastScrollTopRef.current;

    lastScrollTopRef.current = topDistance;

    if (maxScrollDistance < SCROLL_JUMP_MIN_OVERFLOW) {
      programmaticScrollTargetRef.current = null;
      setShowScrollToTop(false);
      setShowScrollToBottom(false);
      return;
    }

    if (programmaticScrollTargetRef.current === 'top') {
      if (isAtTop) {
        programmaticScrollTargetRef.current = null;
      }
      setShowScrollToTop(false);
      setShowScrollToBottom(false);
      return;
    }

    if (programmaticScrollTargetRef.current === 'bottom') {
      if (isAtBottom) {
        programmaticScrollTargetRef.current = null;
      }
      setShowScrollToTop(false);
      setShowScrollToBottom(false);
      return;
    }

    if (Math.abs(scrollDelta) < SCROLL_JUMP_DIRECTION_THRESHOLD) {
      setShowScrollToTop(false);
      setShowScrollToBottom(false);
      return;
    }

    if (scrollIdleTimerRef.current) {
      window.clearTimeout(scrollIdleTimerRef.current);
    }

    scrollIdleTimerRef.current = window.setTimeout(() => {
      scrollIdleTimerRef.current = null;
      setShowScrollToTop(false);
      setShowScrollToBottom(false);
    }, SCROLL_JUMP_IDLE_HIDE_DELAY_MS);

    if (scrollDelta < 0) {
      setShowScrollToTop(!isAtTop);
      setShowScrollToBottom(false);
      return;
    }

    setShowScrollToTop(false);
    setShowScrollToBottom(!isAtBottom);
  }, []);

  const queueScrollJumpVisibilitySync = useCallback(() => {
    if (scrollSyncFrameRef.current) {
      window.cancelAnimationFrame(scrollSyncFrameRef.current);
    }

    scrollSyncFrameRef.current = window.requestAnimationFrame(() => {
      scrollSyncFrameRef.current = null;
      syncScrollJumpVisibility();
    });
  }, [syncScrollJumpVisibility]);

  const handleScrollContainerScroll = useCallback(() => {
    queueScrollJumpVisibilitySync();
  }, [queueScrollJumpVisibilitySync]);

  const handleScrollToTop = useCallback(() => {
    const scrollContainer = scrollContainerRef.current;

    if (!scrollContainer) {
      return;
    }

    programmaticScrollTargetRef.current = 'top';
    setShowScrollToTop(false);
    setShowScrollToBottom(false);
    scrollContainer.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  }, []);

  const handleScrollToBottom = useCallback(() => {
    const scrollContainer = scrollContainerRef.current;

    if (!scrollContainer) {
      return;
    }

    programmaticScrollTargetRef.current = 'bottom';
    setShowScrollToTop(false);
    setShowScrollToBottom(false);
    scrollContainer.scrollTo({
      top: scrollContainer.scrollHeight,
      behavior: 'smooth',
    });
  }, []);

  useEffect(() => {
    const pageElement = pageRef.current;

    if (!pageElement) {
      return undefined;
    }

    const flushDeferredSnapshot = () => {
      if (!deferredSnapshotRef.current) {
        return;
      }

      setSnapshot(deferredSnapshotRef.current);
      deferredSnapshotRef.current = null;
    };

    const handleFocusIn = (event: FocusEvent) => {
      isInteractingRef.current = isSnapshotDeferrableFocusTarget(event.target);
    };

    const handleFocusOut = () => {
      window.setTimeout(() => {
        const activeElement = document.activeElement;
        const stillInside = activeElement instanceof Node && pageElement.contains(activeElement);

        if (stillInside && isSnapshotDeferrableFocusTarget(activeElement)) {
          return;
        }

        isInteractingRef.current = false;
        flushDeferredSnapshot();
      }, 0);
    };

    pageElement.addEventListener('focusin', handleFocusIn);
    pageElement.addEventListener('focusout', handleFocusOut);

    return () => {
      pageElement.removeEventListener('focusin', handleFocusIn);
      pageElement.removeEventListener('focusout', handleFocusOut);
    };
  }, []);

  useEffect(() => {
    const scrollContainer = scrollContainerRef.current;

    if (!scrollContainer) {
      return undefined;
    }

    const resizeObserver =
      typeof ResizeObserver === 'undefined'
        ? null
        : new ResizeObserver(() => {
            queueScrollJumpVisibilitySync();
          });

    lastScrollTopRef.current = scrollContainer.scrollTop;
    syncScrollJumpVisibility();
    window.addEventListener('resize', queueScrollJumpVisibilitySync);
    resizeObserver?.observe(scrollContainer);
    resizeObserver?.observe(scrollContainer.firstElementChild ?? scrollContainer);

    return () => {
      if (scrollSyncFrameRef.current) {
        window.cancelAnimationFrame(scrollSyncFrameRef.current);
        scrollSyncFrameRef.current = null;
      }
      if (scrollIdleTimerRef.current) {
        window.clearTimeout(scrollIdleTimerRef.current);
        scrollIdleTimerRef.current = null;
      }
      window.removeEventListener('resize', queueScrollJumpVisibilitySync);
      resizeObserver?.disconnect();
    };
  }, [queueScrollJumpVisibilitySync, syncScrollJumpVisibility]);

  const scheduleSnapshotRefresh = useCallback(
    (delayMs = 120) => {
      if (!normalizedToken) {
        return;
      }

      if (liveRefreshTimerRef.current != null) {
        window.clearTimeout(liveRefreshTimerRef.current);
      }

      liveRefreshTimerRef.current = window.setTimeout(() => {
        liveRefreshTimerRef.current = null;
        void refreshSnapshot({ silent: true });
      }, delayMs);
    },
    [normalizedToken, refreshSnapshot],
  );

  useEffect(() => {
    if (!normalizedToken) {
      setErrorMessage('공유 링크가 없습니다.');
      setIsLoading(false);
      setHasLoadedShareData(false);
      return undefined;
    }

    void refreshSnapshot({
      initialLoad: true,
      view: 'initial',
      preserveVisibleSnapshot: initialHasCachedSnapshot,
    });
    return undefined;
  }, [initialHasCachedSnapshot, normalizedToken, refreshSnapshot]);
  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    const urlRoomSessionId = readShareRoomSessionIdFromLocation();
    const restoredRoomSessionId = urlRoomSessionId || readStoredShareLastRoomSessionId(normalizedToken);
    const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, restoredRoomSessionId);
    requestedRoomSessionIdRef.current = restoredRoomSessionId;
    deferredSnapshotRef.current = null;
    setSnapshot(cachedSnapshot);
    setIsLoading(cachedSnapshot == null);
    setHasLoadedShareData(cachedSnapshot != null);
    setIsRoomSwitching(false);
    setRequestedRoomSessionId(restoredRoomSessionId);
  }, [normalizedToken]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const handlePopState = () => {
      const nextRoomSessionId = readShareRoomSessionIdFromLocation() || readStoredShareLastRoomSessionId(normalizedToken);
      const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, nextRoomSessionId);
      requestedRoomSessionIdRef.current = nextRoomSessionId;
      deferredSnapshotRef.current = null;
      if (cachedSnapshot) {
        setSnapshot(cachedSnapshot);
      }
      setIsRoomSwitching(Boolean(nextRoomSessionId) && !cachedSnapshot);
      setRequestedRoomSessionId(nextRoomSessionId);
      setIsShareRoomListVisible(false);
    };

    window.addEventListener('popstate', handlePopState);
    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, [normalizedToken]);

  useEffect(() => {
    if (!normalizedToken || !hasSnapshotRef.current) {
      return;
    }

    if (skipNextRequestedRoomRefreshRef.current) {
      skipNextRequestedRoomRefreshRef.current = false;
      return;
    }

    const requestedSessionId = requestedRoomSessionId.trim();
    const hasMatchingSnapshot = doesShareSnapshotMatchRequestedRoom(snapshot, requestedSessionId);
    const mismatchRefreshKey = requestedSessionId ? `snapshot-mismatch:${requestedSessionId}` : ''

    if (hasMatchingSnapshot) {
      if (snapshotMismatchRefreshKeyRef.current) {
        snapshotMismatchRefreshKeyRef.current = '';
      }
      return;
    }

    if (!requestedSessionId || snapshotMismatchRefreshKeyRef.current === mismatchRefreshKey) {
      return;
    }

    snapshotMismatchRefreshKeyRef.current = mismatchRefreshKey;
    void refreshSnapshot({ silent: true, view: 'initial' });
  }, [normalizedToken, refreshSnapshot, requestedRoomSessionId, snapshot]);
  useEffect(() => {
    if (!normalizedToken || !snapshot || snapshot.detailLevel === 'initial') {
      return;
    }

    writeStoredShareRoomSnapshot(normalizedToken, snapshot);
  }, [normalizedToken, snapshot]);

  useEffect(() => {
    if (!normalizedToken || requestedRoomSessionIdRef.current.trim()) {
      return;
    }

    const stabilizedRoomSessionId = activeShareRoomSessionId.trim();

    if (!stabilizedRoomSessionId || !shareRooms.some((room) => room.sessionId === stabilizedRoomSessionId)) {
      return;
    }

    requestedRoomSessionIdRef.current = stabilizedRoomSessionId;
    skipNextRequestedRoomRefreshRef.current = snapshot?.detailLevel === 'initial';
    writeStoredShareLastRoomSessionId(normalizedToken, stabilizedRoomSessionId);
    writeShareRoomSessionIdToLocation(stabilizedRoomSessionId, 'replace');
    setRequestedRoomSessionId(stabilizedRoomSessionId);
  }, [activeShareRoomSessionId, normalizedToken, shareRooms, snapshot?.detailLevel]);

  useEffect(() => {
    if (!requestedRoomSessionId) {
      return;
    }

    if (!snapshot || snapshot.detailLevel === 'initial') {
      return;
    }

    if (shareRooms.some((room) => room.sessionId === requestedRoomSessionId)) {
      return;
    }

    const fallbackRoomSessionId =
      shareRooms.find((room) => room.sessionId === activeShareRoomSessionId)?.sessionId ?? '';

    requestedRoomSessionIdRef.current = fallbackRoomSessionId;
    writeStoredShareLastRoomSessionId(normalizedToken, fallbackRoomSessionId || null);
    setRequestedRoomSessionId(fallbackRoomSessionId);
  }, [activeShareRoomSessionId, normalizedToken, requestedRoomSessionId, shareRooms, snapshot]);
  useEffect(() => {
    if (!normalizedToken) {
      return;
    }

    const persistedRoomSessionId = selectedShareRoomSessionId.trim();

    if (!persistedRoomSessionId) {
      writeStoredShareLastRoomSessionId(normalizedToken, null);
      return;
    }

    if (!shareRooms.some((room) => room.sessionId === persistedRoomSessionId)) {
      return;
    }

    writeStoredShareLastRoomSessionId(normalizedToken, persistedRoomSessionId);
  }, [normalizedToken, selectedShareRoomSessionId, shareRooms]);
  useEffect(() => {
    const roomSessionId = shareRooms.some((room) => room.sessionId === requestedRoomSessionId)
      ? requestedRoomSessionId
      : activeShareRoomSessionId;
    writeShareRoomSessionIdToLocation(roomSessionId || null, 'replace');
  }, [activeShareRoomSessionId, requestedRoomSessionId, shareRooms]);

  const handleUnlockShare = useCallback(async (inputPin?: string) => {
    const normalizedPin = normalizeAccessPinInput(inputPin ?? accessPinInput.trim());

    if (!/^\d{4}$/.test(normalizedPin)) {
      setAccessPinSubmitError('비밀번호는 숫자 4자리로 입력하세요.');
      return;
    }

    setAccessPinInput(normalizedPin);
    setAccessPinSubmitError('');
    await refreshSnapshot({ initialLoad: true, sharePin: normalizedPin });
  }, [accessPinInput, refreshSnapshot]);

  useEffect(() => {
    const sessionExpiresAt = snapshot?.share.accessPinSessionExpiresAt?.trim() ?? '';

    if (!normalizedToken || !snapshot?.share.hasAccessPin || !sessionExpiresAt) {
      return undefined;
    }
    const expiresAtMs = Date.parse(sessionExpiresAt);

    if (!Number.isFinite(expiresAtMs)) {
      return undefined;
    }

    const remainingMs = expiresAtMs - Date.now();

    if (remainingMs <= 0) {
      setStoredChatShareAccessPin(normalizedToken, null);
      setAccessPinInput('');
      setAccessPinSubmitError('유지시간이 지나 비밀번호를 다시 입력해 주세요.');
      setRequiresAccessPin(true);
      return undefined;
    }

    const timeoutId = window.setTimeout(() => {
      setStoredChatShareAccessPin(normalizedToken, null);
      setAccessPinInput('');
      setAccessPinSubmitError('유지시간이 지나 비밀번호를 다시 입력해 주세요.');
      setRequiresAccessPin(true);
    }, remainingMs);

    return () => {
      window.clearTimeout(timeoutId);
    };
  }, [normalizedToken, snapshot?.share.accessPinSessionExpiresAt, snapshot?.share.hasAccessPin]);

  useEffect(() => {
    const sessionId = selectedShareRoomSessionId;

    if (!normalizedToken || !sessionId || typeof window === 'undefined' || requiresAccessPin) {
      return undefined;
    }

    let isDisposed = false;
    let reconnectTimerId: number | null = null;
    let disconnectTimerId: number | null = null;
    let socket: WebSocket | null = null;

    const clearDisconnectTimer = () => {
      if (disconnectTimerId != null) {
        window.clearTimeout(disconnectTimerId);
        disconnectTimerId = null;
      }
    };

    const markDisconnected = () => {
      clearDisconnectTimer();
      disconnectTimerId = window.setTimeout(() => {
        disconnectTimerId = null;

        if (!isDisposed) {
          setIsLiveConnected(false);
        }
      }, 1500);
    };

    const connect = () => {
      if (isDisposed) {
        return;
      }

      const websocketUrl = resolveChatWebSocketUrl(sessionId, undefined, undefined, normalizedToken);

      if (!websocketUrl) {
        setIsLiveConnected(false);
        return;
      }

      socket = new WebSocket(websocketUrl);

      socket.addEventListener('open', () => {
        if (isDisposed) {
          return;
        }

        clearDisconnectTimer();
        setIsLiveConnected(true);
      });

      socket.addEventListener('message', (event) => {
        if (isDisposed || typeof event.data !== 'string') {
          return;
        }

        try {
          const payload = JSON.parse(event.data) as ChatServerEvent;

          if (payload.sessionId !== sessionId) {
            return;
          }

          if (
            payload.type === 'chat:init' ||
            payload.type === 'chat:status' ||
            payload.type === 'chat:runtime' ||
            payload.type === 'chat:runtime:detail' ||
            payload.type === 'notification:messages-updated'
          ) {
            return;
          }
        } catch {
          // 공유 화면은 세부 이벤트를 직접 반영하지 않고 스냅샷만 다시 조회한다.
        }

        scheduleSnapshotRefresh();
      });

      socket.addEventListener('error', () => {
        if (isDisposed) {
          return;
        }

        markDisconnected();
      });

      socket.addEventListener('close', () => {
        if (isDisposed) {
          return;
        }

        markDisconnected();
        reconnectTimerId = window.setTimeout(() => {
          reconnectTimerId = null;
          connect();
        }, 1500);
      });
    };

    connect();

    return () => {
      isDisposed = true;

      if (reconnectTimerId != null) {
        window.clearTimeout(reconnectTimerId);
      }

      clearDisconnectTimer();
      socket?.close();
    };
  }, [normalizedToken, requiresAccessPin, scheduleSnapshotRefresh, selectedShareRoomSessionId]);

  useEffect(() => {
    return () => {
      if (immediateSendHoldTimerRef.current !== null) {
        window.clearTimeout(immediateSendHoldTimerRef.current);
      }
    };
  }, []);

  useEffect(() => {
    const handleResumeSync = () => {
      if (document.visibilityState === 'hidden') {
        return;
      }

      setNowMs(Date.now());
      void refreshSnapshot({ silent: true });
    };

    window.addEventListener('focus', handleResumeSync);
    window.addEventListener('pageshow', handleResumeSync);
    window.addEventListener('online', handleResumeSync);
    document.addEventListener('visibilitychange', handleResumeSync);

    return () => {
      window.removeEventListener('focus', handleResumeSync);
      window.removeEventListener('pageshow', handleResumeSync);
      window.removeEventListener('online', handleResumeSync);
      document.removeEventListener('visibilitychange', handleResumeSync);
    };
  }, [refreshSnapshot]);

  const handleSubmitPrompt = async (
    payload: PromptSubmitPayload & { parentRequestId: string; promptIndex: number; sourceMessageId: number },
  ) => {
    try {
      await submitChatSharePrompt(normalizedToken, {
        sessionId: activeShareRoomSessionId,
        parentRequestId: payload.parentRequestId,
        promptIndex: payload.promptIndex,
        promptTitle: payload.promptTitle,
        promptSignature: payload.promptSignature,
        sourceMessageId: payload.sourceMessageId,
        selectedValues: payload.selection.selectedValues,
        freeText: payload.selection.freeText,
        stepSelections: payload.selection.stepSelections,
        summaryText: payload.selection.summaryText ?? null,
        attachments: payload.selection.attachments ?? payload.attachments ?? [],
        followupText: payload.text,
        mode: payload.mode,
        contextRef: payload.contextRef ?? null,
      });
      message.success('답변을 전송했습니다. 최신 내용을 다시 불러옵니다.');
      void refreshSnapshot({ silent: true });
      return true;
    } catch (error) {
      if (isShareSendDelayError(error)) {
        message.warning('전송 후 응답 확인이 지연되고 있습니다. 연결 복구 시 최신 내용을 다시 불러옵니다.');
        return false;
      }

      message.error(error instanceof Error ? error.message : 'prompt 답변을 전송하지 못했습니다.');
      return false;
    }
  };

  const updatePendingPromptSelection = useCallback((selectionKey: string, selection: PromptDraftSelection | null) => {
    setPendingPromptSelections((current) => {
      if (!selection) {
        if (!(selectionKey in current)) {
          return current;
        }

        const next = { ...current };
        delete next[selectionKey];
        return next;
      }

      return {
        ...current,
        [selectionKey]: {
          ...selection,
          status: 'draft',
        },
      };
    });
  }, []);

  const markPendingPromptSelectionSubmitted = useCallback((selectionKey: string, selection: PromptDraftSelection) => {
    setPendingPromptSelections((current) => ({
      ...current,
      [selectionKey]: {
        ...selection,
        status: 'submitted',
      },
    }));
  }, []);

  const handleCompletePrompt = async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedToken || !normalizedRequestId) {
      return;
    }

    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: 'prompt 완료 처리',
        content: '이 요청의 prompt 선택대기를 수동 완료 처리할까요?',
        okText: '완료 처리',
        cancelText: '취소',
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setPendingPromptCompletionRequestIds((current) => Array.from(new Set([...current, normalizedRequestId])));

    try {
      await completeChatShareManualBadge(normalizedToken, {
        sessionId: activeShareRoomSessionId,
        parentRequestId: normalizedRequestId,
        type: 'prompt',
      });
      message.success('prompt 완료 상태를 저장했습니다.');
      void refreshSnapshot({ silent: true });
    } catch (error) {
      message.error(error instanceof Error ? error.message : 'prompt 완료 상태를 저장하지 못했습니다.');
    } finally {
      setPendingPromptCompletionRequestIds((current) => current.filter((item) => item !== normalizedRequestId));
    }
  };

  const handleCompleteVerification = async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (
      !normalizedToken ||
      !normalizedRequestId ||
      pendingVerificationCompletionRequestIds.includes(normalizedRequestId) ||
      sortedRequests.some(
        (request) => request.requestId === normalizedRequestId && Boolean(request.manualVerificationCompletedAt),
      )
    ) {
      return;
    }

    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: '응답 확인 처리',
        content: '이 요청의 일반 답변을 응답 확인 완료로 처리할까요?',
        okText: '완료 처리',
        cancelText: '취소',
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setPendingVerificationCompletionRequestIds((current) => Array.from(new Set([...current, normalizedRequestId])));

    try {
      const updatedRequest = await completeChatShareManualBadge(normalizedToken, {
        sessionId: activeShareRoomSessionId,
        parentRequestId: normalizedRequestId,
        type: 'verification',
      });
      setSnapshot((current) => (current ? replaceChatShareSnapshotRequest(current, updatedRequest) : current));
      message.success('응답 확인 상태를 저장했습니다.');
    } catch (error) {
      message.error(error instanceof Error ? error.message : '응답 확인 상태를 저장하지 못했습니다.');
    } finally {
      setPendingVerificationCompletionRequestIds((current) => current.filter((item) => item !== normalizedRequestId));
    }
  };

  const handleCancelDisconnectedRequest = async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedToken || !normalizedRequestId || pendingRequestCancellationIds.includes(normalizedRequestId)) {
      return;
    }

    const targetRequest = sortedRequests.find((request) => request.requestId === normalizedRequestId) ?? null;

    if (
      !targetRequest
      || targetRequest.hasResponse
      || targetRequest.status !== 'failed'
      || (targetRequest.statusMessage?.trim() ?? '') !== '중단된 오래된 요청'
    ) {
      return;
    }

    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: '중단 요청 취소 처리',
        content: '연결이 끊겨 멈춘 이 요청을 취소 상태로 정리할까요?',
        okText: '취소 처리',
        cancelText: '닫기',
        okButtonProps: { danger: true },
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setPendingRequestCancellationIds((current) => Array.from(new Set([...current, normalizedRequestId])));

    try {
      const updatedRequest = await cancelChatShareRequest(normalizedToken, {
        sessionId: activeShareRoomSessionId,
        parentRequestId: normalizedRequestId,
      });
      setSnapshot((current) => (current ? replaceChatShareSnapshotRequest(current, updatedRequest) : current));
      message.success('중단된 요청을 취소 처리했습니다.');
      void refreshSnapshot({ silent: true });
    } catch (error) {
      message.error(error instanceof Error ? error.message : '중단 요청 취소 처리에 실패했습니다.');
    } finally {
      setPendingRequestCancellationIds((current) => current.filter((item) => item !== normalizedRequestId));
    }
  };

  const handleRetryDisconnectedRequest = async (requestId: string) => {
    const normalizedRequestId = requestId.trim();

    if (!normalizedToken || !normalizedRequestId || pendingRequestRetryIds.includes(normalizedRequestId)) {
      return;
    }

    const targetRequest = sortedRequests.find((request) => request.requestId === normalizedRequestId) ?? null;

    if (
      !targetRequest
      || targetRequest.hasResponse
      || targetRequest.status !== 'failed'
      || (targetRequest.statusMessage?.trim() ?? '') !== '중단된 오래된 요청'
    ) {
      return;
    }

    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: '중단 요청 재처리',
        content: '연결이 끊겨 멈춘 이 요청을 같은 내용으로 다시 실행할까요?',
        okText: '재처리',
        cancelText: '닫기',
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setPendingRequestRetryIds((current) => Array.from(new Set([...current, normalizedRequestId])));

    try {
      await retryChatShareRequest(normalizedToken, {
        sessionId: activeShareRoomSessionId,
        parentRequestId: normalizedRequestId,
      });
      message.success('중단된 요청 재처리를 시작했습니다. 최신 상태를 다시 불러옵니다.');
      void refreshSnapshot({ silent: true });
    } catch (error) {
      message.error(error instanceof Error ? error.message : '중단 요청 재처리를 시작하지 못했습니다.');
    } finally {
      setPendingRequestRetryIds((current) => current.filter((item) => item !== normalizedRequestId));
    }
  };

  const handleSelectShareRoom = useCallback((sessionId: string) => {
    const normalizedSessionId = sessionId.trim();

    if (!normalizedSessionId) {
      return;
    }

    if (normalizedSessionId === selectedShareRoomSessionId) {
      setIsShareRoomListVisible(false);
      return;
    }

    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);
    requestedRoomSessionIdRef.current = normalizedSessionId;
    writeStoredShareLastRoomSessionId(normalizedToken, normalizedSessionId);
    writeShareRoomSessionIdToLocation(normalizedSessionId, 'push');
    setRequestedRoomSessionId(normalizedSessionId);
    setIsShareRoomListVisible(false);
  }, [normalizedToken, selectedShareRoomSessionId]);

  const handleCreateShareRoom = useCallback(async () => {
    if (!normalizedToken || isCreatingRoom) {
      return;
    }

    const nextChatType = enabledChatTypes.find((item) => item.id === creatingRoomChatTypeId) ?? null;
    const normalizedTitle = creatingRoomTitle.trim();
    const normalizedSeedMessage = creatingRoomSeedMessage.trim();
    const linkedConversation = creatingRoomLinkedSessionId
      ? conversationCandidateBySessionId.get(creatingRoomLinkedSessionId) ?? null
      : null;

    if (!nextChatType) {
      message.warning('새 방에 사용할 채팅유형을 먼저 선택하세요.');
      return;
    }

    if (!normalizedTitle) {
      message.warning('새 채팅방 이름을 입력하세요.');
      return;
    }

    if (!normalizedSeedMessage) {
      message.warning('새 채팅방 시작 문구를 입력하세요.');
      return;
    }

    setIsCreatingRoom(true);

    try {
      const linkedConversationDetail = linkedConversation
        ? await fetchChatConversationDetail(linkedConversation.sessionId, { limit: 20 })
        : null;
      const linkedRequest = linkedConversationDetail?.requests[linkedConversationDetail.requests.length - 1] ?? null;

      if (linkedConversation && !linkedRequest) {
        message.warning('선택한 추천 세션에서 연결할 요청을 찾지 못했습니다.');
        setIsCreatingRoom(false);
        return;
      }

      const createdRoom = await createChatShareRoom(normalizedToken, {
        chatTypeId: nextChatType.id,
        chatTypeLabel: nextChatType.name,
        title: normalizedTitle,
        requestBadgeLabel: creatingRoomRequestBadgeLabel.trim() || null,
        seedMessage: normalizedSeedMessage,
        linkedSessionId: linkedConversation?.sessionId ?? null,
        linkedRequestId: linkedRequest?.requestId ?? null,
        linkedTitle: linkedConversation?.title ?? null,
        linkedRequestPreview:
          linkedRequest?.userText
          || linkedConversation?.lastRequestPreview
          || linkedConversation?.lastMessagePreview
          || null,
        linkedChatTypeLabel: linkedRequest?.chatTypeLabel ?? linkedConversation?.contextLabel ?? null,
      });

      setIsCreateRoomOpen(false);
      setOptimisticShareRooms((current) => (
        current.some((room) => room.sessionId === createdRoom.sessionId)
          ? current
          : [...current, createdRoom]
      ));
      setIsRoomSwitching(true);
      requestedRoomSessionIdRef.current = createdRoom.sessionId;
      writeStoredShareLastRoomSessionId(normalizedToken, createdRoom.sessionId);
      writeShareRoomSessionIdToLocation(createdRoom.sessionId, 'push');
      setRequestedRoomSessionId(createdRoom.sessionId);
      setDraftText('');
      setComposerAttachments([]);
      setReplyReferenceRequestId('');
      setCreatingRoomLinkedSessionId('');
      message.success('새 공유 채팅방을 추가했습니다.');
    } catch (error) {
      message.error(error instanceof Error ? error.message : '새 공유 채팅방을 추가하지 못했습니다.');
    } finally {
      setIsCreatingRoom(false);
    }
  }, [
    creatingRoomChatTypeId,
    creatingRoomLinkedSessionId,
    creatingRoomRequestBadgeLabel,
    creatingRoomSeedMessage,
    creatingRoomTitle,
    conversationCandidateBySessionId,
    enabledChatTypes,
    isCreatingRoom,
    message,
    normalizedToken,
  ]);

  const shareKind = snapshot?.share.kind ?? 'request-bundle';
  const isPromptShare = shareKind === 'prompt';

  const handlePersistSharedComposerModel = useCallback(async (nextCodexModel: string) => {
    const normalizedModel = normalizeCodexModel(nextCodexModel);
    const normalizedScope = appConfig.chat.defaultModelScope;
    const normalizedRoomSessionId = selectedShareRoomSessionId.trim();
    const currentByRoom = appConfig.chat.defaultModelByRoom;

    if (normalizedScope === 'room' && normalizedRoomSessionId) {
      if ((currentByRoom[normalizedRoomSessionId] ?? '') === normalizedModel) {
        return;
      }

      const nextByRoom = {
        ...currentByRoom,
        [normalizedRoomSessionId]: normalizedModel,
      };

      const optimisticConfig = {
        ...appConfig,
        chat: {
          ...appConfig.chat,
          defaultModelByRoom: nextByRoom,
        },
      };

      setStoredAppConfig(optimisticConfig);

      if (!normalizedToken) {
        return;
      }

      try {
        const savedConfig = await saveAppConfigToServer(optimisticConfig, {
          shareToken: normalizedToken,
          skipAutomationNotifications: true,
        });
        setStoredAppConfig(savedConfig);
      } catch {
        // Keep local preference update even if server sync fails.
      }
      return;
    }

    if (appConfig.chat.defaultModelGlobal === normalizedModel) {
      return;
    }

    const optimisticConfig = {
      ...appConfig,
      chat: {
        ...appConfig.chat,
        defaultModelGlobal: normalizedModel,
      },
    };

    setStoredAppConfig(optimisticConfig);

    if (!normalizedToken) {
      return;
    }

    try {
      const savedConfig = await saveAppConfigToServer(optimisticConfig, {
        shareToken: normalizedToken,
        skipAutomationNotifications: true,
      });
      setStoredAppConfig(savedConfig);
    } catch {
      // Keep local preference update even if server sync fails.
    }
  }, [
    appConfig,
    normalizedToken,
    selectedShareRoomSessionId,
  ]);

  const handleSendMessageByMode = useCallback(async (mode: 'queue' | 'direct') => {
    const outgoingText = buildOutgoingShareMessageText(draftText, composerAttachments).trim();
    const resolvedParentRequestId =
      replyReferenceRequestId.trim()
      || (shareKind === 'inquiry-message' ? snapshot?.targetRequest.requestId?.trim() || '' : '')
      || '';

    if (!outgoingText || isSending || isUploadingComposerAttachment) {
      return;
    }

    const nextCodexModel = normalizeCodexModel(sharedComposerCodexModel);
    setIsSending(true);

    try {
      await submitChatShareMessage(normalizedToken, outgoingText, {
        sessionId: selectedShareRoomSessionId,
        mode,
        parentRequestId: resolvedParentRequestId,
        codexModel: nextCodexModel,
      });
      void handlePersistSharedComposerModel(nextCodexModel);
      setDraftText('');
      setComposerAttachments([]);
      setReplyReferenceRequestId('');
      message.success(
        mode === 'direct'
          ? '즉시 전송했습니다. 최신 응답을 다시 불러옵니다.'
          : '대기열에 등록했습니다. 최신 응답을 다시 불러옵니다.',
      );
      void refreshSnapshot({ silent: true });
    } catch (error) {
      if (isShareSendDelayError(error)) {
        setDraftText('');
        setComposerAttachments([]);
        setReplyReferenceRequestId('');
        message.warning(
          mode === 'direct'
            ? '즉시 전송 후 응답 확인이 지연되고 있습니다. 연결 복구 시 최신 내용을 다시 불러옵니다.'
            : '대기열 등록 후 응답 확인이 지연되고 있습니다. 연결 복구 시 최신 내용을 다시 불러옵니다.',
        );
      } else {
        message.error(
          error instanceof Error
            ? error.message
            : mode === 'direct'
              ? '즉시 전송하지 못했습니다.'
              : '대기열에 등록하지 못했습니다.',
        );
      }
    } finally {
      setIsSending(false);
    }
  }, [
    composerAttachments,
    draftText,
    handlePersistSharedComposerModel,
    isSending,
    isUploadingComposerAttachment,
    message,
    normalizedToken,
    refreshSnapshot,
    replyReferenceRequestId,
    selectedShareRoomSessionId,
    shareKind,
    snapshot?.targetRequest.requestId,
    sharedComposerCodexModel,
  ]);

  const handleSendMessage = useCallback(async () => {
    await handleSendMessageByMode(isImmediateSendPinned ? 'direct' : 'queue');
  }, [handleSendMessageByMode, isImmediateSendPinned]);

  const clearImmediateSendHoldTimer = useCallback(() => {
    if (immediateSendHoldTimerRef.current === null) {
      return;
    }

    window.clearTimeout(immediateSendHoldTimerRef.current);
    immediateSendHoldTimerRef.current = null;
  }, []);

  const handleToggleImmediateSendPinned = useCallback(() => {
    if (!normalizedToken) {
      return;
    }

    setImmediateSendPinnedByToken((previous) => {
      const nextState = {
        ...previous,
        [normalizedToken]: previous[normalizedToken] !== true,
      };

      writeStoredShareImmediateSendPinnedByToken(nextState);
      return nextState;
    });
  }, [normalizedToken]);

  const startImmediateSendHoldTimer = useCallback(() => {
    if (isSending || isUploadingComposerAttachment || !normalizedToken) {
      return;
    }

    clearImmediateSendHoldTimer();
    immediateSendHoldTimerRef.current = window.setTimeout(() => {
      immediateSendHoldTimerRef.current = null;
      suppressImmediateSendClickRef.current = true;
      handleToggleImmediateSendPinned();
    }, SHARE_IMMEDIATE_SEND_TOGGLE_HOLD_MS);
  }, [clearImmediateSendHoldTimer, handleToggleImmediateSendPinned, isSending, isUploadingComposerAttachment, normalizedToken]);

  const handleImmediateSendButtonClick = useCallback(() => {
    if (suppressImmediateSendClickRef.current) {
      suppressImmediateSendClickRef.current = false;
      return;
    }

    void handleSendMessageByMode('direct');
  }, [handleSendMessageByMode]);

  const handleComposerKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
    if (event.key !== 'Enter' || event.nativeEvent.isComposing) {
      return;
    }

    if (!event.ctrlKey && !event.metaKey) {
      return;
    }

    event.preventDefault();

    if ((!draftText.trim() && composerAttachments.length === 0) || isSending || isUploadingComposerAttachment) {
      return;
    }

    void handleSendMessageByMode(isImmediateSendPinned ? 'direct' : 'queue');
  };

  const syncFocusedInputIntoView = useCallback((inputTarget?: HTMLElement | null, behavior: ScrollBehavior = 'auto') => {
    if (typeof window === 'undefined' || window.innerWidth > 768) {
      return;
    }

    const scrollContainer = scrollContainerRef.current;
    const composerShell = composerInputShellRef.current;
    const targetInput = inputTarget ?? focusedMobileInputRef.current;
    const target =
      composerShell && targetInput && composerShell.contains(targetInput)
        ? composerShell
        : targetInput;

    if (!scrollContainer || !target) {
      return;
    }

    applyViewportCssVars();
    resetMobileDocumentScrollOffset();

    const containerRect = scrollContainer.getBoundingClientRect();
    const targetRect = target.getBoundingClientRect();
    const visualViewport = window.visualViewport;
    const viewportTop = Math.max(visualViewport?.offsetTop ?? 0, visualViewport?.pageTop ?? 0, 0);
    const viewportBottom = viewportTop + (visualViewport?.height ?? window.innerHeight);
    const visibleTop = Math.max(containerRect.top, viewportTop) + MOBILE_INPUT_VIEWPORT_TOP_PADDING_PX;
    const visibleBottom = Math.min(containerRect.bottom, viewportBottom) - MOBILE_INPUT_VIEWPORT_BOTTOM_PADDING_PX;

    if (visibleBottom <= visibleTop) {
      return;
    }

    if (targetRect.bottom > visibleBottom) {
      const nextScrollTop = scrollContainer.scrollTop + (targetRect.bottom - visibleBottom);
      scrollContainer.scrollTo({ top: Math.max(0, nextScrollTop), behavior });
      return;
    }

    if (targetRect.top < visibleTop) {
      const nextScrollTop = scrollContainer.scrollTop - (visibleTop - targetRect.top);
      scrollContainer.scrollTo({ top: Math.max(0, nextScrollTop), behavior });
    }
  }, []);

  const clearComposerViewportSyncTimers = useCallback(() => {
    if (typeof window === 'undefined') {
      composerFocusScrollTimerIdsRef.current = [];
      return;
    }

    composerFocusScrollTimerIdsRef.current.forEach((timerId) => {
      window.clearTimeout(timerId);
    });
    composerFocusScrollTimerIdsRef.current = [];
  }, []);

  const scheduleFocusedInputViewportSync = useCallback((
    inputTarget?: HTMLElement | null,
    behavior: ScrollBehavior = 'auto',
    options?: { includeRetryTimers?: boolean },
  ) => {
    if (typeof window === 'undefined' || window.innerWidth > 768) {
      return;
    }

    focusedMobileInputRef.current = inputTarget ?? focusedMobileInputRef.current;
    clearComposerViewportSyncTimers();

    window.requestAnimationFrame(() => {
      syncFocusedInputIntoView(inputTarget, behavior);
    });

    if (options?.includeRetryTimers === false) {
      return;
    }

    MOBILE_INPUT_VIEWPORT_SYNC_RETRY_DELAYS_MS.forEach((delay) => {
      const timerId = window.setTimeout(() => {
        syncFocusedInputIntoView(inputTarget, behavior);
        composerFocusScrollTimerIdsRef.current = composerFocusScrollTimerIdsRef.current.filter((value) => value !== timerId);
      }, delay);

      composerFocusScrollTimerIdsRef.current.push(timerId);
    });
  }, [clearComposerViewportSyncTimers, syncFocusedInputIntoView]);

  const syncComposerViewportCompactedState = useCallback(() => {
    if (typeof window === 'undefined') {
      setIsComposerViewportCompacted(false);
      return;
    }

    if (window.innerWidth > 768) {
      setIsComposerViewportCompacted(false);
      return;
    }

    const visualViewport = window.visualViewport;
    const activeElement = document.activeElement;
    const isFocusedShareInput =
      isMobileShareInputTarget(activeElement) && Boolean(scrollContainerRef.current?.contains(activeElement));

    if (!isFocusedShareInput || !visualViewport) {
      setIsComposerViewportCompacted(false);
      return;
    }

    const layoutViewportHeight = Math.max(window.innerHeight || 0, document.documentElement?.clientHeight || 0);
    const visualHeightDelta = Math.max(0, layoutViewportHeight - visualViewport.height);
    const visualOffsetTop = Math.max(visualViewport.offsetTop ?? 0, visualViewport.pageTop ?? 0);

    setIsComposerViewportCompacted(
      visualHeightDelta >= SHARE_COMPOSER_VIEWPORT_COMPACT_THRESHOLD_PX || visualOffsetTop > 0,
    );
  }, []);

  const restoreShareViewportAfterResume = useCallback(() => {
    if (typeof window === 'undefined') {
      return;
    }

    scheduleViewportRecoverySync();

    window.requestAnimationFrame(() => {
      syncComposerViewportCompactedState();

      const activeElement = document.activeElement;
      const resumedFocusTarget =
        isMobileShareInputTarget(activeElement) && scrollContainerRef.current?.contains(activeElement)
          ? activeElement
          : focusedMobileInputRef.current;

      if (!resumedFocusTarget || !scrollContainerRef.current?.contains(resumedFocusTarget)) {
        focusedMobileInputRef.current = null;
        clearComposerViewportSyncTimers();
        return;
      }

      focusedMobileInputRef.current = resumedFocusTarget;
      scheduleFocusedInputViewportSync(resumedFocusTarget, 'auto', { includeRetryTimers: true });
    });
  }, [clearComposerViewportSyncTimers, scheduleFocusedInputViewportSync, syncComposerViewportCompactedState]);

  useEffect(() => {
    return () => {
      clearComposerViewportSyncTimers();
    };
  }, [clearComposerViewportSyncTimers]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const handlePageSuspend = () => {
      clearComposerViewportSyncTimers();
      setIsComposerViewportCompacted(false);
    };

    const handlePageResume = () => {
      restoreShareViewportAfterResume();
    };

    const handleVisibilityChange = () => {
      if (document.visibilityState === 'hidden') {
        handlePageSuspend();
        return;
      }

      handlePageResume();
    };

    window.addEventListener('focus', handlePageResume);
    window.addEventListener('pageshow', handlePageResume);
    window.addEventListener('pagehide', handlePageSuspend);
    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      window.removeEventListener('focus', handlePageResume);
      window.removeEventListener('pageshow', handlePageResume);
      window.removeEventListener('pagehide', handlePageSuspend);
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [clearComposerViewportSyncTimers, restoreShareViewportAfterResume]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const visualViewport = window.visualViewport;

    if (!visualViewport) {
      return undefined;
    }

    const handleViewportChange = () => {
      applyViewportCssVars();
      syncComposerViewportCompactedState();
      const activeElement = document.activeElement;
      const nextFocusTarget = isMobileShareInputTarget(activeElement) ? activeElement : null;

      if (!nextFocusTarget || !scrollContainerRef.current?.contains(nextFocusTarget)) {
        clearComposerViewportSyncTimers();
        return;
      }

      focusedMobileInputRef.current = nextFocusTarget;
      scheduleFocusedInputViewportSync(nextFocusTarget, 'auto', { includeRetryTimers: false });
    };

    visualViewport.addEventListener('resize', handleViewportChange);
    visualViewport.addEventListener('scroll', handleViewportChange);

    return () => {
      visualViewport.removeEventListener('resize', handleViewportChange);
      visualViewport.removeEventListener('scroll', handleViewportChange);
    };
  }, [clearComposerViewportSyncTimers, scheduleFocusedInputViewportSync, syncComposerViewportCompactedState]);

  const handleComposerFocus = () => {
    focusedMobileInputRef.current = composerRef.current?.resizableTextArea?.textArea ?? null;
    syncComposerViewportCompactedState();
    scheduleFocusedInputViewportSync(focusedMobileInputRef.current, 'auto', { includeRetryTimers: true });
  };

  const handleComposerBlur = () => {
    clearComposerViewportSyncTimers();
    setIsComposerViewportCompacted(false);
  };

  const handleScrollContainerFocusCapture = (event: FocusEvent<HTMLDivElement>) => {
    const nextFocusTarget = isMobileShareInputTarget(event.target) ? event.target : null;

    if (!nextFocusTarget) {
      return;
    }

    focusedMobileInputRef.current = nextFocusTarget;
    syncComposerViewportCompactedState();
    const isComposerFocusTarget = Boolean(composerInputShellRef.current?.contains(nextFocusTarget));
    scheduleFocusedInputViewportSync(nextFocusTarget, 'auto', { includeRetryTimers: isComposerFocusTarget });
  };

  const handleScrollContainerBlurCapture = (event: FocusEvent<HTMLDivElement>) => {
    const blurTarget = isMobileShareInputTarget(event.target) ? event.target : null;

    if (!blurTarget) {
      return;
    }

    clearComposerViewportSyncTimers();

    window.setTimeout(() => {
      const activeElement = document.activeElement;

      if (isMobileShareInputTarget(activeElement) && scrollContainerRef.current?.contains(activeElement)) {
        focusedMobileInputRef.current = activeElement;
        syncComposerViewportCompactedState();
        return;
      }

      if (focusedMobileInputRef.current === blurTarget) {
        focusedMobileInputRef.current = null;
      }

      setIsComposerViewportCompacted(false);
    }, 0);
  };

  const handleUploadPromptAttachment = useCallback(
    async (file: File) => {
      const sessionId = selectedShareRoomSessionId;

      if (!sessionId) {
        throw new Error('공유 채팅 세션이 준비되지 않았습니다.');
      }

      return uploadChatShareComposerFile(normalizedToken, sessionId, file);
    },
    [normalizedToken, selectedShareRoomSessionId],
  );

  const handleUploadComposerAttachments = useCallback(
    async (files: File[]) => {
      if (!files.length || isUploadingComposerAttachment) {
        return;
      }

      setIsUploadingComposerAttachment(true);

      try {
        const uploadResults = await Promise.allSettled(files.map((file) => handleUploadPromptAttachment(file)));
        const succeeded = uploadResults
          .filter((result): result is PromiseFulfilledResult<ChatComposerAttachment> => result.status === 'fulfilled')
          .map((result) => result.value);
        const failed = uploadResults.filter((result): result is PromiseRejectedResult => result.status === 'rejected');

        if (succeeded.length > 0) {
          setComposerAttachments((current) => {
            const seenPaths = new Set(current.map((attachment) => attachment.path));

            return [
              ...current,
              ...succeeded.filter((attachment) => {
                if (seenPaths.has(attachment.path)) {
                  return false;
                }

                seenPaths.add(attachment.path);
                return true;
              }),
            ];
          });
        }

        if (failed.length > 0) {
          const firstError = failed[0]?.reason;
          message.error(firstError instanceof Error ? firstError.message : '일부 첨부 파일 업로드에 실패했습니다.');
        }
      } finally {
        setIsUploadingComposerAttachment(false);
      }
    },
    [handleUploadPromptAttachment, isUploadingComposerAttachment, message],
  );

  const handleOpenComposerAttachmentInput = useCallback(() => {
    if (isSending || isUploadingComposerAttachment) {
      return;
    }

    composerAttachmentInputRef.current?.click();
  }, [isSending, isUploadingComposerAttachment]);

  const handleShareComposerAttachmentMenuClick = useCallback(
    ({ key }: { key: string }) => {
      if (key === 'composer-attachment-files') {
        handleOpenComposerAttachmentInput();
      }
    },
    [handleOpenComposerAttachmentInput],
  );


  const handleComposerPaste = useCallback(
    (event: ClipboardEvent<HTMLTextAreaElement>) => {
      const files = resolveShareComposerPasteFiles(event.clipboardData);

      if (files.length === 0 || isSending || isUploadingComposerAttachment) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      void handleUploadComposerAttachments(files);
    },
    [handleUploadComposerAttachments, isSending, isUploadingComposerAttachment],
  );


  const handleClearConversation = useCallback(async () => {
    const sessionId = selectedShareRoomSessionId;
    const conversationTitle = snapshot?.conversation.title?.trim() || '현재 채팅방';

    if (!normalizedToken || !sessionId || isClearingConversation) {
      return;
    }

    const confirmed = await new Promise<boolean>((resolve) => {
      modal.confirm({
        title: '채팅방 데이터를 초기화할까요?',
        content: `"${conversationTitle}" 채팅방의 이름과 설정은 유지되고, 메시지·요청·활동 로그만 초기화됩니다.`,
        okText: '초기화',
        cancelText: '취소',
        okButtonProps: { danger: true },
        centered: true,
        onOk: async () => {
          resolve(true);
        },
        onCancel: () => {
          resolve(false);
        },
      });
    });

    if (!confirmed) {
      return;
    }

    setIsClearingConversation(true);

    try {
      await clearChatShareConversationRoom(normalizedToken, sessionId);
      setDraftText('');
      setComposerAttachments([]);
      setLatestRequestId('');
      setExpandMode('pending');
      setSnapshot((current) =>
        current
          ? {
              ...current,
              requests: [],
              messages: [],
              activityLogs: [],
              roomRequestCounts: {
                processingCount: 0,
                unansweredCount: 0,
              },
              refreshedAt: new Date().toISOString(),
            }
          : current,
      );
      message.success('채팅방 데이터를 초기화했습니다.');
    } catch (error) {
      message.error(error instanceof Error ? error.message : '채팅방 데이터 초기화 중 오류가 발생했습니다.');
    } finally {
      setIsClearingConversation(false);
    }
  }, [isClearingConversation, message, modal, normalizedToken, selectedShareRoomSessionId, snapshot]);
  const handleMoveToSourceSession = useCallback((group: ShareRoomSourceGroup) => {
    if (typeof window === 'undefined' || !group.sourceSessionId) {
      return;
    }

    window.open(`${buildChatPath('live')}?sessionId=${encodeURIComponent(group.sourceSessionId)}`, '_blank', 'noopener,noreferrer');
  }, []);
  const handleSubmitOriginReply = useCallback(async () => {
    if (!normalizedToken || !originReplyTargetGroup?.sourceSessionId || !originReplyTargetGroup.sourceRequestId) {
      return;
    }

    const outgoingText = originReplyDraftText.trim();

    if (!outgoingText) {
      message.warning('원 세션으로 보낼 답변 내용을 입력하세요.');
      return;
    }

    setIsSubmittingOriginReply(true);

    try {
      await submitChatShareOriginReply(normalizedToken, {
        sessionId: selectedShareRoomSessionId,
        sourceSessionId: originReplyTargetGroup.sourceSessionId,
        sourceRequestId: originReplyTargetGroup.sourceRequestId,
        text: outgoingText,
        mode: 'queue',
      });
      setIsOriginReplyModalOpen(false);
      setOriginReplyDraftText('');
      setOriginReplyTargetGroupKey('');
      message.success('원 세션에 답변 전송을 등록했습니다.');
    } catch (error) {
      message.error(error instanceof Error ? error.message : '원 세션 답변 전송에 실패했습니다.');
    } finally {
      setIsSubmittingOriginReply(false);
    }
  }, [
    message,
    normalizedToken,
    originReplyDraftText,
    originReplyTargetGroup?.sourceRequestId,
    originReplyTargetGroup?.sourceSessionId,
    selectedShareRoomSessionId,
  ]);

  const shareBlockedReason = snapshot?.share.blockedReason?.trim() ?? '';
  const canSendMessage = snapshot != null && !isPromptShare && (snapshot.share.canSendMessage ?? true);
  const sortedMessages = useMemo(() => [...(snapshot?.messages ?? [])].sort((left, right) => left.id - right.id), [snapshot]);
  const messageRenderPayloadById = useMemo(
    () => new Map(sortedMessages.map((message) => [message.id, extractShareMessageRenderPayload(message)] as const)),
    [sortedMessages],
  );
  const lastResponseMessage =
    [...sortedMessages].reverse().find((item) => {
      const payload = messageRenderPayloadById.get(item.id);
      return item.author === 'codex' && (payload?.promptParts.length ?? 0) === 0;
    }) ?? null;
  const promptTarget = snapshot?.promptTarget ?? null;
  const promptTargetRequestId = snapshot?.targetRequest?.requestId?.trim() ?? '';
  const requestMessagesById = useMemo(() => {
    const nextMap = new Map<string, ChatMessage[]>();

    sortedMessages.forEach((message) => {
      const requestId = message.clientRequestId?.trim() || '';

      if (!requestId) {
        return;
      }

      const current = nextMap.get(requestId) ?? [];
      current.push(message);
      nextMap.set(requestId, current);
    });

    return nextMap;
  }, [sortedMessages]);
  const childRequestCountByParentId = useMemo(() => buildShareChildRequestCountMap(sortedRequests), [sortedRequests]);
  const promptFollowupCountByParentId = useMemo(() => buildSharePromptFollowupCountMap(sortedRequests), [sortedRequests]);
  const requestById = useMemo(
    () => new Map(sortedRequests.map((request) => [request.requestId.trim(), request])),
    [sortedRequests],
  );
  const promptFollowupStateByPrimaryRequestId = useMemo(
    () =>
      buildSharePromptFollowupStateByPrimaryRequestId(
        sortedRequests,
        requestById,
        requestMessagesById,
        promptFollowupCountByParentId,
        messageRenderPayloadById,
      ),
    [messageRenderPayloadById, promptFollowupCountByParentId, requestById, requestMessagesById, sortedRequests],
  );
  const promptShareFollowupRequests = useMemo(() => {
    if (!isPromptShare || !promptTargetRequestId) {
      return [] as ChatConversationRequest[];
    }

    return sortedRequests.filter((request) => {
      const requestId = request.requestId.trim();

      if (!requestId || requestId === promptTargetRequestId) {
        return false;
      }

      return resolveSharePrimaryRequestId(request, requestById) === promptTargetRequestId;
    });
  }, [isPromptShare, promptTargetRequestId, requestById, sortedRequests]);
  const latestRequest = sortedRequests[sortedRequests.length - 1] ?? null;
  const latestRequestIndex = useMemo(() => {
    if (sortedRequests.length === 0) {
      return -1;
    }

    const matchedIndex = sortedRequests.findIndex((request) => request.requestId === latestRequestId);
    return matchedIndex >= 0 ? matchedIndex : sortedRequests.length - 1;
  }, [latestRequestId, sortedRequests]);
  const currentRequest = latestRequestIndex >= 0 ? sortedRequests[latestRequestIndex] ?? null : latestRequest;
  const pendingCompletionRequests = useMemo(
    () =>
      sortedRequests.filter((request) => {
        return isPendingCompletionShareRequest(
          request,
          requestMessagesById.get(request.requestId) ?? [],
          childRequestCountByParentId,
          promptFollowupCountByParentId,
          messageRenderPayloadById,
        );
      }),
    [childRequestCountByParentId, messageRenderPayloadById, promptFollowupCountByParentId, requestMessagesById, sortedRequests],
  );
  const pendingManualActionRequests = useMemo(
    () =>
      pendingCompletionRequests.filter((request) =>
        isShareRequestAwaitingManualAction(
          request,
          requestMessagesById.get(request.requestId) ?? [],
          childRequestCountByParentId,
          promptFollowupCountByParentId,
          messageRenderPayloadById,
        )),
    [childRequestCountByParentId, messageRenderPayloadById, pendingCompletionRequests, promptFollowupCountByParentId, requestMessagesById],
  );
  const displayedRequests = useMemo(() => {
    if (expandMode === 'all') {
      return sortedRequests;
    }

    if (expandMode === 'pending') {
      return pendingCompletionRequests;
    }

    return currentRequest ? [currentRequest] : [];
  }, [currentRequest, expandMode, pendingCompletionRequests, sortedRequests]);
  const headerInquiryRequest = useMemo(() => {
    if (displayedRequests.length > 0) {
      return displayedRequests[0] ?? null;
    }

    return currentRequest ?? latestRequest ?? sortedRequests[0] ?? null;
  }, [currentRequest, displayedRequests, latestRequest, sortedRequests]);
  const headerTitleText = useMemo(() => {
    const savedConversationTitle = snapshot?.conversation.title?.trim() || '';

    if (savedConversationTitle) {
      return savedConversationTitle;
    }

    return headerInquiryRequest?.userText.trim() || '-';
  }, [headerInquiryRequest?.userText, snapshot?.conversation.title]);
  const hiddenBeforeCount = expandMode === 'latest' && latestRequestIndex >= 0 ? latestRequestIndex : 0;
  const hiddenAfterCount =
    expandMode === 'latest' && latestRequestIndex >= 0 ? Math.max(0, sortedRequests.length - latestRequestIndex - 1) : 0;
  const pendingProcessingCount = pendingCompletionRequests.filter(
    (request) => isRequestInFlight(request.status),
  ).length;
  const pendingUnansweredCount = Math.max(0, pendingCompletionRequests.length - pendingProcessingCount);
  const shareRoomListFetchKey = useMemo(
    () => shareRooms.map((room) => room.sessionId.trim()).filter(Boolean).join('|'),
    [shareRooms],
  );
  const backgroundShareRoomSessionIds = useMemo(
    () =>
      shareRooms
        .map((room) => room.sessionId.trim())
        .filter((sessionId) => sessionId && sessionId !== selectedShareRoomSessionId.trim()),
    [selectedShareRoomSessionId, shareRooms],
  );
  const visibleBackgroundShareRoomSessionIds = useMemo(
    () => (isShareRoomListVisible ? backgroundShareRoomSessionIds : []),
    [backgroundShareRoomSessionIds, isShareRoomListVisible],
  );
  const requestProgressLabel =
    sortedRequests.length === 0
      ? ''
      : expandMode === 'all'
        ? `전체 ${sortedRequests.length}건`
        : expandMode === 'pending'
          ? `처리중 ${pendingProcessingCount}건 · 미확인 ${pendingUnansweredCount}건`
          : `${latestRequestIndex + 1} / ${sortedRequests.length}`;
  const canMoveToPreviousRequest = expandMode === 'latest' && latestRequestIndex > 0;
  const canMoveToNextRequest = expandMode === 'latest' && latestRequestIndex >= 0 && latestRequestIndex < sortedRequests.length - 1;
  const aggregateStatusTag = useMemo(() => {
    if (sortedRequests.length === 0) {
      return null;
    }

    const processingTarget = displayedRequests.find((request) => isRequestProcessing(request.status))
      ?? sortedRequests.find((request) => isRequestProcessing(request.status))
      ?? null;

    if (processingTarget) {
      return {
        ...resolveRequestStatusTag('started'),
        elapsedLabel: formatElapsedDuration(processingTarget.updatedAt?.trim() || processingTarget.createdAt, nowMs),
      };
    }

    const hasPendingPrompt = sortedRequests.some((request) =>
      hasPendingPromptRequest(
        request,
        requestMessagesById.get(request.requestId) ?? [],
        promptFollowupCountByParentId.get(request.requestId.trim()) ?? 0,
        messageRenderPayloadById,
      ));

    if (hasPendingPrompt) {
      return { label: '입력 대기', color: 'warning' as const, elapsedLabel: '' };
    }

    if (sortedRequests.some((request) => request.status === 'queued')) {
      return { ...resolveRequestStatusTag('queued'), elapsedLabel: '' };
    }

    if (pendingCompletionRequests.length > 0) {
      return { label: '확인대기', color: 'warning' as const, elapsedLabel: '' };
    }

    if (sortedRequests.every((request) => request.status === 'completed')) {
      return { ...resolveRequestStatusTag('completed'), elapsedLabel: '' };
    }

    if (sortedRequests.some((request) => request.status === 'failed')) {
      return { label: '일부 실패', color: 'error' as const, elapsedLabel: '' };
    }

    if (sortedRequests.some((request) => request.status === 'cancelled')) {
      return { ...resolveRequestStatusTag('cancelled'), elapsedLabel: '' };
    }

    if (sortedRequests.some((request) => request.status === 'removed')) {
      return { ...resolveRequestStatusTag('removed'), elapsedLabel: '' };
    }

    return { ...resolveRequestStatusTag(sortedRequests[sortedRequests.length - 1]?.status ?? 'queued'), elapsedLabel: '' };
  }, [messageRenderPayloadById, nowMs, pendingCompletionRequests.length, promptFollowupCountByParentId, requestMessagesById, sortedRequests]);
  const requestAnswerTextById = useMemo(() => {
    const nextMap = new Map<string, string>();

    sortedRequests.forEach((request) => {
      nextMap.set(
        request.requestId,
        buildRequestAnswerText(
          request,
          requestMessagesById.get(request.requestId) ?? [],
          messageRenderPayloadById,
        ),
      );
    });

    return nextMap;
  }, [messageRenderPayloadById, requestMessagesById, sortedRequests]);
  const creatingRoomLinkedConversation = useMemo(
    () => (creatingRoomLinkedSessionId ? conversationCandidateBySessionId.get(creatingRoomLinkedSessionId) ?? null : null),
    [conversationCandidateBySessionId, creatingRoomLinkedSessionId],
  );
  useEffect(() => {
    if (!creatingRoomLinkedConversation) {
      return;
    }

    setCreatingRoomTitle((current) => {
      const normalizedCurrent = current.trim();
      if (!normalizedCurrent || normalizedCurrent === '새 공유 채팅방' || normalizedCurrent.endsWith('작업방')) {
        return buildLinkedRoomDraftTitle(creatingRoomLinkedConversation);
      }

      return current;
    });
    setCreatingRoomSeedMessage((current) => {
      const normalizedCurrent = current.trim();
      if (!normalizedCurrent || normalizedCurrent.startsWith('이 방에서 이어갈 작업 내용을')) {
        return buildLinkedRoomDraftSeedMessage(creatingRoomLinkedConversation);
      }

      return current;
    });
  }, [creatingRoomLinkedConversation]);
  const replyReferenceRequest = useMemo(
    () => (replyReferenceRequestId.trim() ? requestById.get(replyReferenceRequestId.trim()) ?? null : null),
    [replyReferenceRequestId, requestById],
  );
  const replyReferenceSummary = useMemo(() => {
    if (!replyReferenceRequest) {
      return '';
    }

    const answerText =
      requestAnswerTextById.get(replyReferenceRequest.requestId.trim())
      || replyReferenceRequest.responseText
      || replyReferenceRequest.statusMessage
      || '';

    return summarizeShareReplyReferenceText(answerText || replyReferenceRequest.userText || '선택한 답변');
  }, [replyReferenceRequest, requestAnswerTextById]);
  const previousQuestionModalTargetRequest = useMemo(
    () => (previousQuestionModalRequestId.trim() ? requestById.get(previousQuestionModalRequestId.trim()) ?? null : null),
    [previousQuestionModalRequestId, requestById],
  );
  const previousQuestionModalLineage = useMemo(
    () => resolveShareRequestLineage(previousQuestionModalTargetRequest, requestById),
    [previousQuestionModalTargetRequest, requestById],
  );
  const previousQuestionModalDirectParent = previousQuestionModalLineage.directParentRequest;
  const previousQuestionModalTopParent =
    previousQuestionModalLineage.topParentRequest
    && previousQuestionModalLineage.topParentRequest.requestId.trim() !== previousQuestionModalLineage.directParentRequest?.requestId.trim()
      ? previousQuestionModalLineage.topParentRequest
      : null;
  const previousQuestionModalDirectParentText = useMemo(
    () => resolveShareRequestQuestionText(previousQuestionModalDirectParent),
    [previousQuestionModalDirectParent],
  );
  const previousQuestionModalTopParentText = useMemo(
    () => resolveShareRequestQuestionText(previousQuestionModalTopParent),
    [previousQuestionModalTopParent],
  );
  const previousQuestionModalDirectParentPreviewItems = useMemo(
    () => buildSharePreviewItemsFromText(previousQuestionModalDirectParent?.userText ?? '', normalizedToken),
    [normalizedToken, previousQuestionModalDirectParent?.userText],
  );
  const previousQuestionModalTopParentPreviewItems = useMemo(
    () => buildSharePreviewItemsFromText(previousQuestionModalTopParent?.userText ?? '', normalizedToken),
    [normalizedToken, previousQuestionModalTopParent?.userText],
  );
  const activityLogByRequestId = useMemo(
    () => new Map((snapshot?.activityLogs ?? []).map((item) => [item.requestId.trim(), item])),
    [snapshot?.activityLogs],
  );
  const activeProcessInspectorRequest = useMemo(
    () => (activeProcessInspectorRequestId.trim() ? requestById.get(activeProcessInspectorRequestId.trim()) ?? null : null),
    [activeProcessInspectorRequestId, requestById],
  );
  const isProcessInspectorSummaryCollapsed = processInspectorExpandedSection !== 'summary';
  const isProcessInspectorChecklistCollapsed = processInspectorExpandedSection !== 'checklist';
  const isProcessInspectorNarrativesCollapsed = processInspectorExpandedSection !== 'narratives';
  const isProcessInspectorLogCollapsed = processInspectorExpandedSection !== 'log';
  const activeProcessInspectorPayload = useMemo(() => {
    if (!activeProcessInspectorRequest) {
      return null;
    }

    const runtimeItem = shareRuntimeItemByRequestId.get(activeProcessInspectorRequest.requestId.trim()) ?? null;
    const activityLines = activityLogByRequestId.get(activeProcessInspectorRequest.requestId.trim())?.lines ?? [];
    return buildShareProcessInspectorPayload(activeProcessInspectorRequest, activityLines, nowMs, runtimeItem);
  }, [activeProcessInspectorRequest, activityLogByRequestId, nowMs, shareRuntimeItemByRequestId]);
  const hasActiveProcessInspector = activeProcessInspectorRequestId.trim().length > 0 && activeProcessInspectorPayload != null;
  useEffect(() => {
    if (!hasActiveProcessInspector) {
      processInspectorDragStateRef.current = null;
      return;
    }

    const clampPosition = (position: { x: number; y: number }) => {
      const cardWidth = processInspectorCardRef.current?.offsetWidth ?? SHARE_PROCESS_INSPECTOR_DEFAULT_WIDTH;
      const cardHeight = processInspectorCardRef.current?.offsetHeight ?? SHARE_PROCESS_INSPECTOR_DEFAULT_HEIGHT;
      const maxX = Math.max(
        SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING,
        window.innerWidth - cardWidth - SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING,
      );
      const maxY = Math.max(
        SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING,
        window.innerHeight - cardHeight - SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING,
      );

      return {
        x: clampProgramMinimizedValue(position.x, SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING, maxX),
        y: clampProgramMinimizedValue(position.y, SHARE_PROCESS_INSPECTOR_VIEWPORT_PADDING, maxY),
      };
    };

    const syncPosition = (position: { x: number; y: number }) => {
      const nextPosition = clampPosition(position);
      processInspectorPositionRef.current = nextPosition;
      setProcessInspectorPosition(nextPosition);
    };

    const handleResize = () => {
      syncPosition(processInspectorPositionRef.current);
    };

    const handlePointerMove = (event: PointerEvent) => {
      const dragState = processInspectorDragStateRef.current;

      if (!dragState || dragState.pointerId !== event.pointerId) {
        return;
      }

      const deltaX = event.clientX - dragState.lastX;
      const deltaY = event.clientY - dragState.lastY;
      dragState.lastX = event.clientX;
      dragState.lastY = event.clientY;

      syncPosition({
        x: processInspectorPositionRef.current.x + deltaX,
        y: processInspectorPositionRef.current.y + deltaY,
      });
    };

    const finishPointerDrag = (event: PointerEvent) => {
      const dragState = processInspectorDragStateRef.current;

      if (!dragState || dragState.pointerId !== event.pointerId) {
        return;
      }

      if (dragState.captureTarget.hasPointerCapture(event.pointerId)) {
        dragState.captureTarget.releasePointerCapture(event.pointerId);
      }

      processInspectorDragStateRef.current = null;
    };

    window.addEventListener('resize', handleResize);
    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', finishPointerDrag);
    window.addEventListener('pointercancel', finishPointerDrag);
    handleResize();

    return () => {
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', finishPointerDrag);
      window.removeEventListener('pointercancel', finishPointerDrag);
    };
  }, [activeProcessInspectorRequestId, hasActiveProcessInspector, processInspectorMode]);
  const processInspectorCard = activeProcessInspectorPayload ? (
    <div
      ref={processInspectorCardRef}
      className={`chat-share-page__process-inspector chat-share-page__process-inspector--${processInspectorMode}`}
      style={{
        transform:
          processInspectorMode === 'fullscreen'
            ? 'translate3d(0, 0, 0)'
            : `translate3d(${processInspectorPosition.x}px, ${processInspectorPosition.y}px, 0)`,
        zIndex: SHARE_PROCESS_INSPECTOR_Z_INDEX,
      }}
    >
      <div className="chat-share-page__process-inspector-drag" onPointerDown={handleProcessInspectorPointerDown}>
        <div className="chat-share-page__process-inspector-drag-copy">
          <span className="chat-share-page__process-inspector-drag-grip" aria-hidden="true" />
          <div className="chat-share-page__process-inspector-drag-text">
            <Text strong>상세 과정</Text>
            <Text type="secondary" className="chat-share-page__process-inspector-request-id">
              {activeProcessInspectorPayload.requestId}
            </Text>
          </div>
        </div>
        <div className="chat-share-page__process-inspector-window-actions">
          <Button
            type="text"
            size="small"
            className="chat-share-page__process-inspector-window-button"
            icon={<MinusOutlined />}
            aria-label="최소화"
            onClick={() => {
              setProcessInspectorMode('minimized');
            }}
          />
          <Button
            type="text"
            size="small"
            className="chat-share-page__process-inspector-window-button"
            icon={processInspectorMode === 'fullscreen' ? <AppstoreOutlined /> : <FullscreenOutlined />}
            aria-label={processInspectorMode === 'fullscreen' ? '일반 크기' : '전체화면'}
            onClick={() => {
              setProcessInspectorMode((current) => (current === 'fullscreen' ? 'default' : 'fullscreen'));
            }}
          />
          <Button
            type="text"
            size="small"
            className="chat-share-page__process-inspector-window-button"
            icon={<ShareCloseIcon />}
            aria-label="상세 과정 닫기"
            onClick={closeProcessInspector}
          />
        </div>
      </div>
      {processInspectorMode === 'minimized' ? (
        <div className="chat-share-page__process-inspector-minimized">
          <div className="chat-share-page__process-inspector-minimized-copy">
            <div className="chat-share-page__process-inspector-minimized-head">
              <Tag color={activeProcessInspectorPayload.statusTag.color}>{activeProcessInspectorPayload.statusTag.label}</Tag>
              <Text className="chat-share-page__process-inspector-minimized-time">{activeProcessInspectorPayload.elapsedLabel}</Text>
            </div>
            <Text className="chat-share-page__process-inspector-minimized-log">
              {activeProcessInspectorPayload.latestActivityLine || '활동 로그 대기 중'}
            </Text>
          </div>
          <Button
            type="primary"
            size="small"
            className="chat-share-page__process-inspector-minimized-button"
            onClick={() => {
              setProcessInspectorMode('default');
            }}
          >
            열기
          </Button>
        </div>
      ) : (
        <div className="chat-share-page__process-inspector-body">
          <div className="chat-share-page__process-inspector-summary">
            <div className="chat-share-page__process-inspector-summary-head">
              <div className="chat-share-page__process-inspector-summary-head-main">
                <Tag color={activeProcessInspectorPayload.statusTag.color}>{activeProcessInspectorPayload.statusTag.label}</Tag>
                <Text type="secondary">{`처리 시간 ${activeProcessInspectorPayload.elapsedLabel}`}</Text>
              </div>
              <Button
                type="text"
                size="small"
                className="chat-share-page__process-inspector-summary-toggle"
                icon={isProcessInspectorSummaryCollapsed ? <DownOutlined /> : <UpOutlined />}
                onClick={handleToggleProcessInspectorSummary}
              >
                {isProcessInspectorSummaryCollapsed ? '요청 정보 보기' : '요청 정보 접기'}
              </Button>
            </div>
            {isProcessInspectorSummaryCollapsed ? null : (
              <div className="chat-share-page__process-inspector-summary-table" role="table" aria-label="상세 과정 요약">
                <div className="chat-share-page__process-inspector-table-row" role="row">
                  <Text type="secondary" className="chat-share-page__process-inspector-table-label">요청</Text>
                  <Text strong className="chat-share-page__process-inspector-table-value">{activeProcessInspectorPayload.summary}</Text>
                </div>
                <div className="chat-share-page__process-inspector-table-row" role="row">
                  <Text type="secondary" className="chat-share-page__process-inspector-table-label">ID</Text>
                  <Text className="chat-share-page__process-inspector-table-value chat-share-page__process-inspector-table-value--mono">
                    {activeProcessInspectorPayload.requestId}
                  </Text>
                </div>
                <div className="chat-share-page__process-inspector-table-row" role="row">
                  <Text type="secondary" className="chat-share-page__process-inspector-table-label">시작</Text>
                  <Text className="chat-share-page__process-inspector-table-value">{activeProcessInspectorPayload.startedAtLabel}</Text>
                </div>
                <div className="chat-share-page__process-inspector-table-row" role="row">
                  <Text type="secondary" className="chat-share-page__process-inspector-table-label">업데이트</Text>
                  <Text className="chat-share-page__process-inspector-table-value">{activeProcessInspectorPayload.updatedAtLabel}</Text>
                </div>
                <div className="chat-share-page__process-inspector-table-row" role="row">
                  <Text type="secondary" className="chat-share-page__process-inspector-table-label">최근 로그</Text>
                  <Text className="chat-share-page__process-inspector-table-value">{activeProcessInspectorPayload.latestActivityLine || '아직 활동 로그가 없습니다.'}</Text>
                </div>
              </div>
            )}
          </div>
          <div
            className={[
              'chat-share-page__process-inspector-sections',
              processInspectorMode === 'fullscreen' ? 'chat-share-page__process-inspector-sections--fullscreen' : '',
              isProcessInspectorChecklistCollapsed ? 'chat-share-page__process-inspector-sections--checklist-collapsed' : '',
              isProcessInspectorNarrativesCollapsed ? 'chat-share-page__process-inspector-sections--narratives-collapsed' : '',
              isProcessInspectorLogCollapsed ? 'chat-share-page__process-inspector-sections--log-collapsed' : '',
            ].filter(Boolean).join(' ')}
          >
            <section className="chat-share-page__process-inspector-section chat-share-page__process-inspector-section--checklist">
              <div className="chat-share-page__process-inspector-section-head">
                <Text strong>계획 체크리스트</Text>
                <Button
                  type="text"
                  size="small"
                  className="chat-share-page__process-inspector-summary-toggle"
                  icon={isProcessInspectorChecklistCollapsed ? <DownOutlined /> : <UpOutlined />}
                  onClick={handleToggleProcessInspectorChecklist}
                >
                  {isProcessInspectorChecklistCollapsed ? '보기' : '접기'}
                </Button>
              </div>
              {isProcessInspectorChecklistCollapsed ? null : (
                <div className="chat-share-page__process-inspector-checklist" role="table" aria-label="계획 체크리스트">
                  {activeProcessInspectorPayload.checklist.map((step) => (
                    <div key={step.key} className="chat-share-page__process-inspector-check-item" role="row">
                      <Text strong className="chat-share-page__process-inspector-check-title">{step.label}</Text>
                      <Tag
                        color={
                          step.status === 'completed'
                            ? 'success'
                            : step.status === 'in_progress'
                              ? 'processing'
                              : 'default'
                        }
                      >
                        {step.status === 'completed' ? '완료' : step.status === 'in_progress' ? '진행중' : '대기'}
                      </Tag>
                      <Text type="secondary" className="chat-share-page__process-inspector-check-note">{step.note}</Text>
                    </div>
                  ))}
                </div>
              )}
            </section>
            <section className="chat-share-page__process-inspector-section chat-share-page__process-inspector-section--narratives">
              <div className="chat-share-page__process-inspector-section-head">
                <Text strong>추가 실행 설명</Text>
                <Button
                  type="text"
                  size="small"
                  className="chat-share-page__process-inspector-summary-toggle"
                  icon={isProcessInspectorNarrativesCollapsed ? <DownOutlined /> : <UpOutlined />}
                  onClick={handleToggleProcessInspectorNarratives}
                >
                  {isProcessInspectorNarrativesCollapsed ? '보기' : '접기'}
                </Button>
              </div>
              {isProcessInspectorNarrativesCollapsed ? null : (
                <div className="chat-share-page__process-inspector-narratives" role="table" aria-label="추가 실행 설명">
                  {activeProcessInspectorPayload.narratives.map((item, index) => (
                    <div key={item} className="chat-share-page__process-inspector-narrative" role="row">
                      <Text type="secondary" className="chat-share-page__process-inspector-table-label">{String(index + 1).padStart(2, '0')}</Text>
                      <Text className="chat-share-page__process-inspector-table-value">{item}</Text>
                    </div>
                  ))}
                </div>
              )}
            </section>
            <section className="chat-share-page__process-inspector-section chat-share-page__process-inspector-section--log">
              <div className="chat-share-page__process-inspector-section-head">
                <Text strong>활동 로그</Text>
                <div className="chat-share-page__process-inspector-section-head-actions">
                  <Text type="secondary">{activeProcessInspectorPayload.activityLines.length}줄</Text>
                  <Button
                    type="text"
                    size="small"
                    className="chat-share-page__process-inspector-summary-toggle"
                    icon={isProcessInspectorLogCollapsed ? <DownOutlined /> : <UpOutlined />}
                    onClick={handleToggleProcessInspectorLog}
                  >
                    {isProcessInspectorLogCollapsed ? '보기' : '접기'}
                  </Button>
                </div>
              </div>
              {isProcessInspectorLogCollapsed ? null : (
                <div className="chat-share-page__process-inspector-log" role="table" aria-label="활동 로그">
                  {activeProcessInspectorPayload.activityLines.length > 0 ? (
                    activeProcessInspectorPayload.activityLines.map((line, index) => (
                      <div key={`${activeProcessInspectorPayload.requestId}:${index}`} className="chat-share-page__process-inspector-log-line" role="row">
                        <span className="chat-share-page__process-inspector-log-index">{String(index + 1).padStart(2, '0')}</span>
                        <span className="chat-share-page__process-inspector-log-text">{line}</span>
                      </div>
                    ))
                  ) : (
                    <Text type="secondary">활동 로그가 아직 기록되지 않았습니다.</Text>
                  )}
                </div>
              )}
            </section>
          </div>
        </div>
      )}
    </div>
  ) : null;
  const shareChatTypeLabel = useMemo(() => {
    const candidates = [
      currentRequest?.chatTypeLabel,
      snapshot?.targetRequest?.chatTypeLabel,
      ...sortedRequests.map((request) => request.chatTypeLabel),
    ];

    return candidates.find((value) => value?.trim())?.trim() || 'Codex Live';
  }, [currentRequest?.chatTypeLabel, snapshot?.targetRequest?.chatTypeLabel, sortedRequests]);
  const shareComposerResolvedCodexModel = useMemo(() => {
    const normalizedRoomSessionId = selectedShareRoomSessionId.trim();
    const chatModelScope = appConfig.chat.defaultModelScope;
    const roomModelText =
      chatModelScope === 'room' ? appConfig.chat.defaultModelByRoom[normalizedRoomSessionId]?.trim() ?? '' : '';

    if (chatModelScope === 'room' && normalizedRoomSessionId && roomModelText) {
      return normalizeCodexModel(roomModelText);
    }

    if (appConfig.chat.defaultModelGlobal?.trim()) {
      return normalizeCodexModel(appConfig.chat.defaultModelGlobal);
    }

    const candidates = [
      currentRequest?.codexModel,
      snapshot?.targetRequest?.codexModel,
      ...sortedRequests.map((request) => request.codexModel),
    ];

    return normalizeCodexModel(candidates.find((value) => value?.trim())?.trim());
  }, [
    appConfig.chat.defaultModelByRoom,
    appConfig.chat.defaultModelGlobal,
    appConfig.chat.defaultModelScope,
    currentRequest?.codexModel,
    selectedShareRoomSessionId,
    sortedRequests,
    snapshot?.targetRequest?.codexModel,
  ]);
  useEffect(() => {
    setSharedComposerCodexModel((current) => {
      const nextModel = normalizeCodexModel(shareComposerResolvedCodexModel);
      return normalizeCodexModel(current) === nextModel ? current : nextModel;
    });
  }, [shareComposerResolvedCodexModel]);
  const shareComposerModelUsedTokens = useMemo(() => {
    const normalizedModel = normalizeCodexModel(sharedComposerCodexModel);

    return sortedRequests.reduce((accumulator, request) => {
      if (resolveRequestCodexModel(request) !== normalizedModel) {
        return accumulator;
      }

      return accumulator + resolveRequestUsageTokens(request);
    }, 0);
  }, [sharedComposerCodexModel, sortedRequests]);
  const shareComposerModelTokenLabel = useMemo(() => '이번 세션 사용량 ' + formatTokenCount(shareComposerModelUsedTokens) + ' 토큰', [shareComposerModelUsedTokens]);
  const shareMenuLabel = useMemo(() => {
    const candidates = [
      snapshot?.conversation.requestBadgeLabel,
      snapshot?.conversation.title,
      headerInquiryRequest?.chatTypeLabel,
      currentRequest?.chatTypeLabel,
    ];

    return candidates.find((value) => value?.trim())?.trim() || '';
  }, [currentRequest?.chatTypeLabel, headerInquiryRequest?.chatTypeLabel, snapshot?.conversation.requestBadgeLabel, snapshot?.conversation.title]);
  const collapsedActivitySummary = useMemo(() => {
    if (expandMode !== 'latest' || !currentRequest || !isRequestProcessing(currentRequest.status)) {
      return [] as string[];
    }

    const activityLog = activityLogByRequestId.get(currentRequest.requestId.trim());
    return summarizeActivityLogLines(activityLog?.lines ?? []);
  }, [activityLogByRequestId, currentRequest, expandMode]);
  const headerSummaryLabel = useMemo(
    () =>
      `처리 시간 ${aggregateStatusTag?.elapsedLabel || '-'} · 처리중 ${pendingProcessingCount}건 · 미확인 ${pendingUnansweredCount}건`,
    [aggregateStatusTag?.elapsedLabel, pendingProcessingCount, pendingUnansweredCount],
  );

  useEffect(() => {
    const sessionId = selectedShareRoomSessionId.trim();

    if (!sessionId) {
      return;
    }

    setShareRoomPendingCountsBySessionId((current) => {
      const previousCounts = current[sessionId];

      if (
        previousCounts?.processingCount === pendingProcessingCount
        && previousCounts?.unansweredCount === pendingUnansweredCount
      ) {
        return current;
      }

      return {
        ...current,
        [sessionId]: {
          processingCount: pendingProcessingCount,
          unansweredCount: pendingUnansweredCount,
        },
      };
    });
  }, [pendingProcessingCount, pendingUnansweredCount, selectedShareRoomSessionId]);

  const refreshShareRoomPendingCount = useCallback(async (sessionId: string) => {
    const normalizedSessionId = sessionId.trim();

    if (!normalizedToken || !normalizedSessionId) {
      return;
    }

    const inFlightTask = shareRoomPendingCountRefreshPromiseBySessionIdRef.current[normalizedSessionId];

    if (inFlightTask) {
      shareRoomPendingCountRefreshQueuedBySessionIdRef.current[normalizedSessionId] = true;
      await inFlightTask;
      return;
    }

    const sharePin = getStoredChatShareAccessPin(normalizedToken) || undefined;
    const refreshTask = (async () => {
      try {
        const roomSnapshot = await fetchChatShareSnapshot(normalizedToken, {
          sessionId: normalizedSessionId,
          view: 'initial',
          sharePin,
        });
        writeStoredShareRoomSnapshot(normalizedToken, roomSnapshot);

        setShareRoomPendingCountsBySessionId((current) => {
          const nextCounts = resolveShareRoomPendingCounts(roomSnapshot);
          const previousCounts = current[normalizedSessionId];

          if (
            previousCounts?.processingCount === nextCounts.processingCount
            && previousCounts?.unansweredCount === nextCounts.unansweredCount
          ) {
            return current;
          }

          return {
            ...current,
            [normalizedSessionId]: nextCounts,
          };
        });
      } catch (error) {
        console.error('failed to refresh share room pending counts', normalizedSessionId, error);
      } finally {
        shareRoomPendingCountRefreshPromiseBySessionIdRef.current[normalizedSessionId] = null;

        if (shareRoomPendingCountRefreshQueuedBySessionIdRef.current[normalizedSessionId]) {
          shareRoomPendingCountRefreshQueuedBySessionIdRef.current[normalizedSessionId] = false;
          window.setTimeout(() => {
            void refreshShareRoomPendingCount(normalizedSessionId);
          }, 0);
        }
      }
    })();

    shareRoomPendingCountRefreshPromiseBySessionIdRef.current[normalizedSessionId] = refreshTask;
    await refreshTask;
  }, [normalizedToken]);

  useEffect(() => {
    if (!normalizedToken || !shareRoomListFetchKey) {
      return;
    }

    const targetRoomSessionIds = visibleBackgroundShareRoomSessionIds;

    if (targetRoomSessionIds.length === 0) {
      setIsLoadingShareRoomPendingCounts(false);
      return;
    }

    const fetchSequence = shareRoomPendingCountFetchSequenceRef.current + 1;
    shareRoomPendingCountFetchSequenceRef.current = fetchSequence;
    let cancelled = false;
    setIsLoadingShareRoomPendingCounts(true);

    void Promise.allSettled(
      targetRoomSessionIds.map(async (sessionId) => {
        await refreshShareRoomPendingCount(sessionId);
      }),
    )
      .finally(() => {
        if (cancelled || shareRoomPendingCountFetchSequenceRef.current !== fetchSequence) {
          return;
        }

        setIsLoadingShareRoomPendingCounts(false);
      });

    return () => {
      cancelled = true;
    };
  }, [normalizedToken, refreshShareRoomPendingCount, shareRoomListFetchKey, visibleBackgroundShareRoomSessionIds]);

  useEffect(() => {
    if (!normalizedToken || requiresAccessPin || typeof window === 'undefined' || visibleBackgroundShareRoomSessionIds.length === 0) {
      return undefined;
    }

    let isDisposed = false;
    const sockets = new Map<string, WebSocket>();
    const reconnectTimerIds = new Map<string, number>();
    const refreshTimerIds = new Map<string, number>();

    const clearRefreshTimer = (sessionId: string) => {
      const timerId = refreshTimerIds.get(sessionId);
      if (timerId != null) {
        window.clearTimeout(timerId);
        refreshTimerIds.delete(sessionId);
      }
    };

    const scheduleRoomCountRefresh = (sessionId: string) => {
      if (isDisposed || refreshTimerIds.has(sessionId)) {
        return;
      }

      const timerId = window.setTimeout(() => {
        refreshTimerIds.delete(sessionId);
        void refreshShareRoomPendingCount(sessionId);
      }, 150);

      refreshTimerIds.set(sessionId, timerId);
    };

    const connectRoomSocket = (sessionId: string) => {
      if (isDisposed) {
        return;
      }

      const websocketUrl = resolveChatWebSocketUrl(sessionId, undefined, undefined, normalizedToken);

      if (!websocketUrl) {
        return;
      }

      const socket = new WebSocket(websocketUrl);
      sockets.set(sessionId, socket);

      socket.addEventListener('message', (event) => {
        if (isDisposed || typeof event.data !== 'string') {
          return;
        }

        try {
          const payload = JSON.parse(event.data) as ChatServerEvent;

          if (payload.sessionId !== sessionId) {
            return;
          }

          if (
            payload.type === 'chat:init'
            || payload.type === 'chat:status'
            || payload.type === 'chat:runtime'
            || payload.type === 'chat:runtime:detail'
            || payload.type === 'notification:messages-updated'
          ) {
            return;
          }
        } catch {
          // payload 해석 실패 시에도 해당 방 건수만 다시 확인한다.
        }

        scheduleRoomCountRefresh(sessionId);
      });

      socket.addEventListener('error', () => {
        if (isDisposed) {
          return;
        }

        socket.close();
      });

      socket.addEventListener('close', () => {
        sockets.delete(sessionId);
        clearRefreshTimer(sessionId);

        if (isDisposed) {
          return;
        }

        const reconnectTimerId = window.setTimeout(() => {
          reconnectTimerIds.delete(sessionId);
          connectRoomSocket(sessionId);
        }, 1500);

        reconnectTimerIds.set(sessionId, reconnectTimerId);
      });
    };

    visibleBackgroundShareRoomSessionIds.forEach((sessionId) => {
      connectRoomSocket(sessionId);
    });

    return () => {
      isDisposed = true;

      reconnectTimerIds.forEach((timerId) => {
        window.clearTimeout(timerId);
      });
      refreshTimerIds.forEach((timerId) => {
        window.clearTimeout(timerId);
      });
      sockets.forEach((socket) => {
        socket.close();
      });
    };
  }, [normalizedToken, refreshShareRoomPendingCount, requiresAccessPin, visibleBackgroundShareRoomSessionIds]);

  useEffect(() => {
    if (!activeProcessInspectorRequestId.trim()) {
      return;
    }

    if (activeProcessInspectorRequest) {
      return;
    }

    setActiveProcessInspectorRequestId('');
  }, [activeProcessInspectorRequest, activeProcessInspectorRequestId]);

  useEffect(() => {
    if (!replyReferenceRequestId.trim()) {
      return;
    }

    if (requestById.has(replyReferenceRequestId.trim())) {
      return;
    }

    setReplyReferenceRequestId('');
  }, [replyReferenceRequestId, requestById]);

  useEffect(() => {
    if (sortedRequests.length === 0) {
      if (latestRequestId) {
        setLatestRequestId('');
      }
      return;
    }

    const nextLatestRequest = sortedRequests[sortedRequests.length - 1];
    if (!nextLatestRequest) {
      return;
    }

    if (latestRequestId && sortedRequests.some((request) => request.requestId === latestRequestId)) {
      return;
    }

    if (latestRequestId !== nextLatestRequest.requestId) {
      setLatestRequestId(nextLatestRequest.requestId);
    }
  }, [latestRequestId, sortedRequests]);

  const handleMoveToPreviousRequest = () => {
    if (!canMoveToPreviousRequest) {
      return;
    }

    const previousRequest = sortedRequests[latestRequestIndex - 1];
    if (previousRequest) {
      setLatestRequestId(previousRequest.requestId);
    }
  };

  const handleMoveToNextRequest = () => {
    if (!canMoveToNextRequest) {
      return;
    }

    const nextRequest = sortedRequests[latestRequestIndex + 1];
    if (nextRequest) {
      setLatestRequestId(nextRequest.requestId);
    }
  };
  const hasActiveProcessingRequest = useMemo(
    () => displayedRequests.some((request) => isRequestProcessing(request.status)),
    [displayedRequests],
  );
  const contentLayoutClassName = canSendMessage
    ? 'chat-share-page__content-layout chat-share-page__content-layout--with-composer'
    : 'chat-share-page__content-layout';
  const canToggleShareRoomList = shareRooms.length > 1 || canCreateSharedRooms;
  const captureProgramRestoreSnapshot = useCallback(
    (): ShareProgramRestoreSnapshot => ({
      roomSessionId: selectedShareRoomSessionId.trim(),
      latestRequestId: latestRequestId.trim(),
      expandMode,
      scrollTop: Math.max(0, scrollContainerRef.current?.scrollTop ?? 0),
    }),
    [expandMode, latestRequestId, selectedShareRoomSessionId],
  );
  const restoreProgramReturnSnapshot = useCallback((restoreSnapshot?: ShareProgramRestoreSnapshot | null) => {
    if (typeof window === 'undefined' || !restoreSnapshot) {
      return;
    }

    const normalizedRoomSessionId = restoreSnapshot.roomSessionId.trim();

    if (normalizedRoomSessionId && normalizedRoomSessionId !== selectedShareRoomSessionId) {
      const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, normalizedRoomSessionId);
      if (cachedSnapshot) {
        setSnapshot(cachedSnapshot);
      }
      setIsRoomSwitching(!cachedSnapshot);
      requestedRoomSessionIdRef.current = normalizedRoomSessionId;
      writeStoredShareLastRoomSessionId(normalizedToken, normalizedRoomSessionId);
      writeShareRoomSessionIdToLocation(normalizedRoomSessionId, 'replace');
      setRequestedRoomSessionId(normalizedRoomSessionId);
    }

    setExpandMode(restoreSnapshot.expandMode);
    setLatestRequestId(restoreSnapshot.latestRequestId.trim());
    setIsSearchOpen(false);
    setIsShareRoomListVisible(false);

    const applyScrollPosition = () => {
      const scrollContainer = scrollContainerRef.current;

      if (!scrollContainer) {
        return;
      }

      const maxScrollTop = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight);
      const nextScrollTop = Math.min(Math.max(0, restoreSnapshot.scrollTop), maxScrollTop);

      scrollContainer.scrollTo({
        top: nextScrollTop,
        behavior: 'auto',
      });
      lastScrollTopRef.current = nextScrollTop;
      queueScrollJumpVisibilitySync();
    };

    window.requestAnimationFrame(() => {
      applyScrollPosition();
      window.setTimeout(applyScrollPosition, 80);
    });
  }, [normalizedToken, queueScrollJumpVisibilitySync, selectedShareRoomSessionId]);
  const handleCloseProgram = useCallback(() => {
    restoreProgramReturnSnapshot(programTarget?.restoreSnapshot);
    setProgramTarget(null);
  }, [programTarget?.restoreSnapshot, restoreProgramReturnSnapshot]);
  function canLaunchShareProgram(appId?: ShareProgramTarget['appId']) {
    if (!appId) {
      return true;
    }

    if (appId === SHARE_CURRENT_CHAT_APP_ID) {
      return true;
    }

    if (appId === 'app-settings') {
      return canManageSharedAppSettings;
    }

    return shareAllowedAppIdSet.has(appId);
  }
  const recordShareAppLaunch = useCallback((appId?: string) => {
    if (!appId) {
      return;
    }

    setAppLaunchUsage((current) => {
      const nextRecord = {
        count: (current[appId]?.count ?? 0) + 1,
        lastOpenedAt: Date.now(),
      };
      const nextValue = {
        ...current,
        [appId]: nextRecord,
      };
      writeShareAppLaunchUsage(nextValue);
      return nextValue;
    });
  }, []);
  const openProgramTarget = useCallback((target: ShareProgramTarget) => {
    if (!canLaunchShareProgram(target.appId)) {
      message.warning('이 공유 링크에서는 허용되지 않은 앱입니다.');
      return;
    }

    recordShareAppLaunch(target.appId);
    setProgramReloadKey(0);
    setMinimizedPrograms((current) => current.filter((item) => item.target.key !== target.key));
    setProgramTarget({
      ...target,
      restoreSnapshot: target.restoreSnapshot ?? captureProgramRestoreSnapshot(),
    });
  }, [canLaunchShareProgram, captureProgramRestoreSnapshot, message, recordShareAppLaunch]);
  const openAllowedPlayAppEnvironment = useCallback((entry: PlayAppEntry, environment: ShareAppEnvironment) => {
    if (!shareAllowedAppIdSet.has(entry.id)) {
      message.warning('이 공유 링크에서는 허용되지 않은 앱입니다.');
      return;
    }

    if (!isPlayAppSupportedInEnvironment(entry, environment)) {
      const environmentLabel = SHARE_APP_ENVIRONMENT_OPTIONS.find((item) => item.key === environment)?.label ?? environment;
      const supportedLabel = resolveSupportedEnvironmentSummary(entry);
      message.warning(`${entry.name} 앱은 ${environmentLabel} 환경에서 아직 열 수 없습니다. 지원 환경: ${supportedLabel}`);
      return;
    }

    setIsSearchOpen(false);
    openProgramTarget(buildPlayAppEnvironmentTarget(entry.id, entry.name, environment));
  }, [message, openProgramTarget, shareAllowedAppIdSet]);
  const handleMinimizeProgram = useCallback(() => {
    if (!programTarget) {
      return;
    }

    setMinimizedPrograms((current) => {
      const existingIndex = current.findIndex((item) => item.target.key === programTarget.key);
      const rememberedPosition = minimizedProgramPositionByKeyRef.current[programTarget.key];
      const nextPosition = existingIndex >= 0
        ? current[existingIndex].position
        : rememberedPosition ?? getStackedProgramMinimizedPosition(current.length);
      const nextItem: ShareMinimizedProgramItem = {
        target: programTarget,
        position: nextPosition,
      };
      minimizedProgramPositionByKeyRef.current[programTarget.key] = nextPosition;

      if (existingIndex >= 0) {
        return current.map((item, index) => (index === existingIndex ? nextItem : item));
      }

      return [...current, nextItem];
    });
    setProgramTarget(null);
    restoreProgramReturnSnapshot(programTarget.restoreSnapshot);
  }, [programTarget, restoreProgramReturnSnapshot]);
  const handleSearchResultSelect = useCallback((result: ShareSearchResult) => {
    if (result.appEntry) {
      openAllowedPlayAppEnvironment(result.appEntry, selectedAppEnvironment);
      return;
    }

    if (result.resource && !result.requestId && !result.scrollTarget) {
      setIsSearchOpen(false);
      openProgramTarget(result.resource);
      return;
    }

    setIsSearchOpen(false);

    if (result.requestId) {
      setLatestRequestId(result.requestId);
      setExpandMode('all');
    }

    const scrollTarget = result.scrollTarget;

    if (!scrollTarget) {
      return;
    }

    window.requestAnimationFrame(() => {
      window.setTimeout(() => {
        if (scrollTarget.type === 'request') {
          if (scrollToShareAnchorElement(requestAnchorRefs.current.get(scrollTarget.value))) {
            return;
          }
        } else if (scrollTarget.type === 'response') {
          const messageId = Number(scrollTarget.value);
          if (Number.isFinite(messageId) && scrollToShareAnchorElement(responseAnchorRefs.current.get(messageId))) {
            return;
          }
        } else if (scrollTarget.type === 'prompt') {
          if (scrollToShareAnchorElement(promptAnchorRefs.current.get(scrollTarget.value))) {
            return;
          }
        } else if (scrollToShareAnchorElement(document.getElementById(scrollTarget.value))) {
          return;
        }

        const selector =
          scrollTarget.type === 'request'
            ? `#chat-share-request-${CSS.escape(scrollTarget.value)}`
            : `#${CSS.escape(scrollTarget.value)}`;
        document.querySelector<HTMLElement>(selector)?.scrollIntoView({
          behavior: 'smooth',
          block: scrollTarget.type === 'activity' ? 'center' : 'start',
        });
      }, 80);
    });
  }, [openAllowedPlayAppEnvironment, openProgramTarget, selectedAppEnvironment]);
  const closeProgramTarget = useCallback(() => {
    setProgramTarget(null);
  }, []);
  const sharedServerCommandAccess = useMemo(
    () => ({
      shareToken: normalizedToken,
      allowedKeys: ['test', 'rel', 'prod', 'work-server', 'command-runner'] as const,
    }),
    [normalizedToken],
  );
  const shouldInlineProgramTarget = shouldRenderSharePlayAppInline(programTarget);
  const embeddedPlayAppContent = shouldInlineProgramTarget && programTarget
    ? renderEmbeddedSharePlayApp(programTarget.appId, closeProgramTarget, normalizedToken)
    : null;
  const isServerCommandDrawerOpen = programTarget?.appId === 'server-command';
  const isAppSettingsDrawerOpen = programTarget?.appId === 'app-settings';
  const indexedContentSearchResults = useMemo<ShareSearchIndexedResult[]>(() => {
    const results: ShareSearchIndexedResult[] = [];

    sortedRequests.forEach((request) => {
      const requestText = buildShareVisibleText(request.userText);

      results.push({
        key: `request:${request.requestId}`,
        title: requestText || '질문',
        description: `질문 · ${formatTimeLabel(request.createdAt)}`,
        category: 'request',
        requestId: request.requestId,
        scrollTarget: {
          type: 'request',
          value: request.requestId,
        },
        searchText: buildSearchKeywordText(requestText, request.statusMessage, request.requestId),
      });

      buildSharePreviewItemsFromText(request.userText, normalizedToken).forEach((item) => {
        results.push({
          key: `request-resource:${request.requestId}:${item.id}`,
          title: item.label,
          description: `질문 리소스 · ${item.kind}`,
          category: 'resource',
          requestId: request.requestId,
          resource: {
            key: `request-resource:${request.requestId}:${item.id}`,
            label: item.label,
            url: item.url,
            kind: item.kind,
            meta: `${item.kind} resource`,
          },
          scrollTarget: {
            type: 'request',
            value: request.requestId,
          },
          searchText: buildSearchKeywordText(item.label, item.url, item.kind),
        });
      });
    });

    sortedMessages.forEach((entry) => {
      const payload = messageRenderPayloadById.get(entry.id);
      const visibleText = buildVisibleMessageText(entry, payload);
      const requestId = entry.clientRequestId?.trim() || snapshot?.rootRequestId?.trim() || '';

      results.push({
        key: `response:${entry.id}`,
        title: visibleText || '응답',
        description: `${entry.author === 'user' ? '질문 메시지' : '응답'} · ${formatTimeLabel(entry.timestamp)}`,
        category: 'response',
        requestId: requestId || undefined,
        scrollTarget: {
          type: 'response',
          value: String(entry.id),
        },
        searchText: buildSearchKeywordText(visibleText, entry.author, requestId),
      });

      (payload?.promptParts ?? []).forEach((prompt, promptIndex) => {
        results.push({
          key: `prompt:${entry.id}:${promptIndex}`,
          title: prompt.title || 'prompt',
          description: `prompt · ${formatTimeLabel(entry.timestamp)}`,
          category: 'response',
          requestId: requestId || undefined,
          scrollTarget: {
            type: 'prompt',
            value: buildSharePromptAnchorKey(entry.id, promptIndex),
          },
          searchText: buildSearchKeywordText(buildSharePromptSearchText(prompt), requestId, String(entry.id)),
        });
      });

      (payload?.previewItems ?? []).forEach((item) => {
        if (!requestId) {
          return;
        }

        results.push({
          key: `response-resource:${entry.id}:${item.id}`,
          title: item.label,
          description: `응답 리소스 · ${item.kind}`,
          category: 'resource',
          requestId,
          resource: {
            key: `response-resource:${entry.id}:${item.id}`,
            label: item.label,
            url: resolveShareScopedResourceUrl(item.url, normalizedToken),
            kind: item.kind,
            meta: `${item.kind} resource`,
          },
          scrollTarget: {
            type: 'response',
            value: String(entry.id),
          },
          searchText: buildSearchKeywordText(item.label, item.url, item.kind),
        });
      });
    });

    (snapshot?.activityLogs ?? []).forEach((activity, index) => {
      const summary = summarizeActivityLogLines(activity.lines ?? []).join(' ');

      results.push({
        key: `activity:${activity.requestId}:${index}`,
        title: summary || `활동 로그 ${index + 1}`,
        description: `활동 로그 · ${activity.requestId}`,
        category: 'activity',
        requestId: activity.requestId,
        scrollTarget: {
          type: 'activity',
          value: 'chat-share-activity-panel',
        },
        searchText: buildSearchKeywordText(activity.requestId, summary),
      });
    });

    return results.filter((item) => !item.scrollTarget || item.scrollTarget.value.trim());
  }, [messageRenderPayloadById, normalizedToken, snapshot?.activityLogs, snapshot?.rootRequestId, sortedMessages, sortedRequests]);
  const indexedAppSearchResults = useMemo<ShareSearchIndexedResult[]>(() => {
    const results: ShareSearchIndexedResult[] = [];
    const photoprismLauncher = buildPhotoPrismProgramTarget();

    results.push({
      key: `management-app:${SHARE_CURRENT_CHAT_APP_ID}`,
      title: currentShareChatTarget.label,
      description: `현재 공유토큰을 ${selectedAppEnvironment} 환경에서 다시 엽니다.`,
      category: 'resource',
      icon: <CommentOutlined />,
      usageBadge: resolveShareAppUsageBadge(appLaunchUsage[SHARE_CURRENT_CHAT_APP_ID]),
      resource: currentShareChatTarget,
      searchText: buildSearchKeywordText(
        currentShareChatTarget.label,
        currentShareChatTarget.appId,
        '공유채팅',
        '현재 토큰',
        '현재 공유토큰 열기',
        ...APPS_LAUNCHER_SEARCH_TERMS,
      ),
    });

    sortedAllowedManagementApps.forEach((item) => {
      results.push({
        key: `management-app:${item.value}`,
        title: item.label,
        description: item.description,
        category: 'resource',
        icon: item.icon,
        usageBadge: resolveShareAppUsageBadge(appLaunchUsage[item.value]),
        resource: buildShareManagementProgramTarget(item.value, item.label),
        searchText: buildSearchKeywordText(item.value, item.label, item.description, ...APPS_LAUNCHER_SEARCH_TERMS),
      });
    });

    sortedAllowedPlayAppEntries.forEach((entry) => {
      results.push({
        key: `app:${entry.id}`,
        title: entry.name,
        description: resolveSupportedEnvironmentSummary(entry),
        category: 'resource',
        icon: entry.icon,
        usageBadge: resolveShareAppUsageBadge(appLaunchUsage[entry.id]),
        appEntry: entry,
        resource:
          entry.id === 'photoprism'
            ? photoprismLauncher
            : buildPlayAppProgramTarget(entry.id, entry.name),
        searchText: buildSearchKeywordText(
          entry.id,
          entry.name,
          entry.searchDescription,
          ...APPS_LAUNCHER_SEARCH_TERMS,
          ...(entry.searchKeywords ?? []),
        ),
      });
    });

    return results;
  }, [appLaunchUsage, currentShareChatTarget, selectedAppEnvironment, sortedAllowedManagementApps, sortedAllowedPlayAppEntries]);
  const searchResults = useMemo<ShareSearchResult[]>(() => {
    const keyword = normalizeSearchKeyword(searchKeyword);
    const source = searchPanelMode === 'apps' ? indexedAppSearchResults : indexedContentSearchResults;

    if (searchPanelMode !== 'apps' && !keyword) {
      return [];
    }

    const filtered = keyword
      ? source.filter((item) => item.searchText.includes(keyword))
      : source;

    return filtered.slice(0, 40).map(({ searchText: _searchText, ...result }) => result);
  }, [indexedAppSearchResults, indexedContentSearchResults, searchKeyword, searchPanelMode]);
  const selectedTokenUsageSetting = shareTokenSetting;
  const tokenUsageSummaryByPeriod = useMemo(
    () =>
      Object.fromEntries(
        TOKEN_USAGE_PERIODS.map((period) => [
          period.key,
          resolveTokenUsageWindowSummary(
            sortedRequests,
            period.key,
            nowMs,
            resolveTokenUsageLimitForPeriod(selectedTokenUsageSetting, period.key),
          ),
        ]),
      ) as Record<TokenUsagePeriodKey, TokenUsageWindowSummary>,
    [nowMs, selectedTokenUsageSetting, sortedRequests],
  );
  const tokenUsageFiveHourSummary = tokenUsageSummaryByPeriod['5h'];
  const tokenUsageSevenDaySummary = tokenUsageSummaryByPeriod['7d'];
  const tokenUsageOverview = useMemo(() => {
    const sevenDayLimit = Math.max(0, Math.round(Number(resolveTokenUsageLimitForPeriod(selectedTokenUsageSetting, '7d')) || 0));
    const fiveHourLimit = Math.max(0, Math.round(Number(resolveTokenUsageLimitForPeriod(selectedTokenUsageSetting, '5h')) || 0));
    const sevenDayRemaining = tokenUsageSevenDaySummary.remainingTokens;
    const fiveHourRemaining = tokenUsageFiveHourSummary.remainingTokens;
    const currentAvailableTokens = resolveSmallestFiniteNumber(sevenDayRemaining, fiveHourRemaining);
    const formatUsagePercentLabel = (value: number | null) =>
      value == null ? '미표시' : String(Math.round(value)) + '%';
    const axisLimit = sevenDayLimit > 0 ? sevenDayLimit : Math.max(fiveHourLimit, currentAvailableTokens ?? 0);
    const overallLabel = sevenDayLimit > 0 ? `${formatTokenCount(sevenDayLimit)} 토큰` : '무제한';
    const sevenDayRemainingLabel = sevenDayRemaining == null ? '무제한' : `${formatTokenCount(sevenDayRemaining)} 토큰`;
    const fiveHourRemainingLabel = fiveHourRemaining == null ? '무제한' : `${formatTokenCount(fiveHourRemaining)} 토큰`;
    const currentAvailableLabel = currentAvailableTokens == null ? '무제한' : `${formatTokenCount(currentAvailableTokens)} 토큰`;
    const resolveAxisWidthPercent = (value: number | null, fallback: number) => {
      if (axisLimit <= 0) {
        return fallback;
      }

      if (value == null) {
        return fallback;
      }

      return Math.max(4, Math.min(100, (value / axisLimit) * 100));
    };

    return {
      currentAvailableLabel,
      currentPercentLabel: formatUsagePercentLabel(
        resolveSmallestFiniteNumber(tokenUsageSevenDaySummary.percentage, tokenUsageFiveHourSummary.percentage),
      ),
      sevenDayUsagePercentLabel: formatUsagePercentLabel(
        tokenUsageSevenDaySummary.percentage == null
          ? null
          : Math.max(0, Math.min(100, tokenUsageSevenDaySummary.percentage)),
      ),
      fiveHourUsagePercentLabel: formatUsagePercentLabel(
        tokenUsageFiveHourSummary.percentage == null
          ? null
          : Math.max(0, Math.min(100, tokenUsageFiveHourSummary.percentage)),
      ),
      overallLabel,
      sevenDayRemainingLabel,
      fiveHourRemainingLabel,
      fiveHourBucketLabel: `5시간 버킷 ${fiveHourRemainingLabel}`,
      oneLineSummary:
        sevenDayLimit > 0
          ? `전체는 1주일 총량 ${overallLabel}, 1주일은 현재 남은 ${sevenDayRemainingLabel}, 5시간은 주간 잔여 상한이 적용된 실제 사용 가능 ${currentAvailableLabel}`
          : `현재 사용 가능 ${currentAvailableLabel}`,
      fiveHourResetAmountLabel:
        tokenUsageFiveHourSummary.nextUsageDropAt && tokenUsageFiveHourSummary.nextResetTokens > 0
          ? `${formatTokenCount(tokenUsageFiveHourSummary.nextResetTokens)} 토큰`
          : tokenUsageFiveHourSummary.nextUsageDropAt
            ? '일부 초기화'
            : '예정 없음',
      fiveHourResetTimeLabel: tokenUsageFiveHourSummary.nextUsageDropAt
        ? formatTimeLabel(tokenUsageFiveHourSummary.nextUsageDropAt) || '시간 미기록'
        : '예정 없음',
      fiveHourCountdownLabel: formatCountdownLabel(tokenUsageFiveHourSummary.nextUsageDropAt, nowMs),
      sevenDayResetTimeLabel: tokenUsageSevenDaySummary.nextUsageDropAt
        ? formatTimeLabel(tokenUsageSevenDaySummary.nextUsageDropAt) || '시간 미기록'
        : '예정 없음',
      meterLegendItems: [
        {
          key: 'overall',
          label: '전체',
          colorClassName: 'chat-share-page__token-usage-meter-fill--overall',
          caption: overallLabel,
        },
        {
          key: '7d',
          label: '1주일',
          colorClassName: 'chat-share-page__token-usage-meter-fill--7d',
          caption: sevenDayRemainingLabel,
        },
        {
          key: '5h',
          label: '5시간',
          colorClassName: 'chat-share-page__token-usage-meter-fill--5h',
          caption: currentAvailableLabel,
        },
      ],
      meterLayers: [
        {
          key: 'overall',
          colorClassName: 'chat-share-page__token-usage-meter-fill--overall',
          widthPercent: 100,
        },
        {
          key: '7d',
          colorClassName: 'chat-share-page__token-usage-meter-fill--7d',
          widthPercent: resolveAxisWidthPercent(sevenDayRemaining, 100),
        },
        {
          key: '5h',
          colorClassName: 'chat-share-page__token-usage-meter-fill--5h',
          widthPercent: resolveAxisWidthPercent(currentAvailableTokens, 100),
        },
      ],
    };
  }, [nowMs, selectedTokenUsageSetting, tokenUsageFiveHourSummary, tokenUsageSevenDaySummary]);
  const currentShareUrl = useMemo(() => buildAbsoluteShareUrl(snapshot?.share.sharePath), [snapshot?.share.sharePath]);
  const currentShareEffectiveExpiresAt = useMemo(() => resolveShareEffectiveExpiresAt(snapshot?.share ?? null), [snapshot?.share]);
  const currentShareDefaultExpiryLabel = useMemo(() => {
    const label = formatDurationMinutesLabel(snapshot?.share.tokenSetting?.defaultExpiresInMinutes);
    return label ? `기본 유효시간 ${label}` : '';
  }, [snapshot?.share.tokenSetting?.defaultExpiresInMinutes]);
  const currentShareDefaultPolicyLabel = useMemo(
    () => currentShareDefaultExpiryLabel || '사용기간 제한 없음',
    [currentShareDefaultExpiryLabel],
  );
  const currentShareExpiresAtLabel = useMemo(
    () =>
      currentShareEffectiveExpiresAt
        ? formatShareExpirySummary(currentShareEffectiveExpiresAt, nowMs)
        : currentShareDefaultPolicyLabel,
    [currentShareDefaultPolicyLabel, currentShareEffectiveExpiresAt, nowMs],
  );
  const currentShareRemainingTimeLabel = useMemo(
    () => (currentShareEffectiveExpiresAt ? formatRemainingTimeLabel(currentShareEffectiveExpiresAt, nowMs) : ''),
    [currentShareEffectiveExpiresAt, nowMs],
  );
  const currentShareTokenUsageStatusLabel = useMemo(
    () => currentShareRemainingTimeLabel || currentShareDefaultPolicyLabel,
    [currentShareDefaultPolicyLabel, currentShareRemainingTimeLabel],
  );
  const handleCopyCurrentShareUrl = useCallback(async () => {
    if (!currentShareUrl) {
      return;
    }

    try {
      await copyTextToClipboard(currentShareUrl);
      message.success('토큰 URL을 복사했습니다.');
    } catch (error) {
      console.error('failed to copy share url', error);
      message.error('토큰 URL 복사에 실패했습니다.');
    }
  }, [currentShareUrl, message]);
  const shareHeaderSettingsItems = useMemo<MenuProps['items']>(
    () => [
      {
        key: 'conversation-title',
        label: (
          <span className="chat-share-page__settings-item chat-share-page__settings-item--summary">
            <span className="chat-share-page__settings-item-title">
              {snapshot?.conversation.title?.trim() || '현재 채팅방'}
            </span>
            <span className="chat-share-page__settings-item-description">현재 공유 채팅방</span>
          </span>
        ),
        disabled: true,
      },
      {
        key: 'conversation-search',
        label: (
          <span className="chat-share-page__settings-item">
            <span className="chat-share-page__settings-item-title">통합검색</span>
            <span className="chat-share-page__settings-item-description">질문, 답변, 리소스, 활동 로그를 함께 찾습니다.</span>
          </span>
        ),
        icon: <SearchOutlined />,
      },
      {
        key: 'conversation-refresh',
        label: (
          <span className="chat-share-page__settings-item">
            <span className="chat-share-page__settings-item-title">강력 새로고침</span>
            <span className="chat-share-page__settings-item-description">
              서비스워커, 캐시, 공유채팅 임시 저장 상태를 정리한 뒤 현재 공유채팅방 화면을 다시 불러옵니다.
            </span>
          </span>
        ),
        icon: <ReloadOutlined />,
      },
      ...(normalizedToken || allowedManagementApps.length > 0 || allowedPlayAppEntries.length > 0
        ? [
            {
              key: 'conversation-apps',
              label: (
                <span className="chat-share-page__settings-item">
                  <span className="chat-share-page__settings-item-title">Apps</span>
                  <span className="chat-share-page__settings-item-description">
                    허용된 앱을 빠르게 확인하고 실행합니다.
                  </span>
                </span>
              ),
              icon: <AppstoreOutlined />,
            },
          ]
        : []),
      {
        key: 'conversation-token-usage',
        label: (
          <span className="chat-share-page__settings-item chat-share-page__settings-item--token">
            <span className="chat-share-page__settings-item-title-row">
              <span className="chat-share-page__settings-item-title">토큰 관리</span>
              <span className="chat-share-page__settings-item-meta">
                {selectedTokenUsageSetting ? selectedTokenUsageSetting.name : '설정 필요'}
              </span>
            </span>
            <span className="chat-share-page__settings-item-description">
                {selectedTokenUsageSetting
                  ? '지금 사용 가능 ' + tokenUsageOverview.currentAvailableLabel + ' (5시간 ' + tokenUsageOverview.fiveHourUsagePercentLabel + ' / 1주일 ' + tokenUsageOverview.sevenDayUsagePercentLabel + ')'
                : '공유 링크 생성 시 선택된 토큰 설정이 없습니다.'}
            </span>
            <span className="chat-share-page__settings-item-description">
              5시간 초기화 {tokenUsageOverview.fiveHourCountdownLabel}
            </span>
            <span className="chat-share-page__settings-meter" aria-hidden="true">
              <span
                className="chat-share-page__settings-meter-fill"
                style={{
                  width: `${
                    selectedTokenUsageSetting && tokenUsageFiveHourSummary.percentage != null
                      ? Math.max(6, tokenUsageFiveHourSummary.percentage)
                      : 18
                  }%`,
                }}
              />
            </span>
          </span>
        ),
        icon: <SettingOutlined />,
      },
      ...(canOpenSharedRoomSettings
        ? [
            {
              key: 'conversation-room-settings',
              label: (
                <span className="chat-share-page__settings-item">
                  <span className="chat-share-page__settings-item-title">채팅방 설정</span>
                  <span className="chat-share-page__settings-item-description">
                    Codex Live와 동일한 Context 기준으로 유형과 문맥을 조정합니다.
                  </span>
                </span>
              ),
              icon: <AppstoreOutlined />,
            },
          ]
        : []),
      ...(canManageSharedAppSettings
        ? [
            {
              key: 'conversation-personal-settings',
              label: (
                <span className="chat-share-page__settings-item">
                  <span className="chat-share-page__settings-item-title">개인설정</span>
                  <span className="chat-share-page__settings-item-description">
                    앱 전체 웹 푸시, 유도 문구, 클라이언트 저장값을 탭으로 관리합니다.
                  </span>
                </span>
              ),
              icon: <SettingOutlined />,
            },
          ]
        : []),
      ...(hasWorkServerCommandApp
        ? [
            {
              key: 'conversation-work-server-command',
              label: (
                <span className="chat-share-page__settings-item">
                  <span className="chat-share-page__settings-item-title-row">
                    <span className="chat-share-page__settings-item-title">
                      <span
                        className={`chat-share-page__settings-version-indicator chat-share-page__settings-version-indicator--${resolveShareWorkServerVersionStatus(shareWorkServerCommand)}`}
                        aria-hidden="true"
                      />
                      서버관리
                    </span>
                    <span
                      className={`chat-share-page__settings-status-badge chat-share-page__settings-status-badge--${resolveShareWorkServerVersionStatus(shareWorkServerCommand)}`}
                    >
                      {resolveShareWorkServerStatusLabel(shareWorkServerCommand)}
                    </span>
                  </span>
                  <span className="chat-share-page__settings-item-description">
                    {resolveShareWorkServerVersionDescription(shareWorkServerCommand)}
                  </span>
                  <span className="chat-share-page__settings-item-meta chat-share-page__settings-item-meta--detail">
                    {resolveShareWorkServerVersionLabel(shareWorkServerCommand)}
                  </span>
                </span>
              ),
              icon: <ReloadOutlined />,
            },
          ]
        : []),
      {
        key: 'conversation-clear',
        label: (
          <span className="chat-share-page__settings-item">
            <span className="chat-share-page__settings-item-title">채팅방 비우기</span>
            <span className="chat-share-page__settings-item-description">메시지, 요청, 활동 로그를 초기화합니다.</span>
          </span>
        ),
        icon: <DeleteOutlined />,
        danger: true,
        disabled: !canSendMessage || isClearingConversation || !selectedShareRoomSessionId,
      },
    ],
    [
      allowedManagementApps.length,
      allowedPlayAppEntries.length,
      canSendMessage,
      canOpenSharedRoomSettings,
      hasWorkServerCommandApp,
      isClearingConversation,
      normalizedToken,
      selectedTokenUsageSetting,
      shareAllowedAppIdSet,
      sharePermissionSet,
      shareWorkServerCommand,
      snapshot?.conversation.title,
      selectedShareRoomSessionId,
        tokenUsageFiveHourSummary.percentage,
        tokenUsageOverview.currentAvailableLabel,
        tokenUsageOverview.fiveHourCountdownLabel,
      ],
    );
  const openShareAppsPanel = useCallback(() => {
    setSearchPanelMode('apps');
    setSearchKeyword('');
    setIsShareRoomListVisible(false);
    setIsSearchOpen(true);
  }, []);
  const handleShareHeaderSettingsClick = useCallback<NonNullable<MenuProps['onClick']>>(
    ({ key }) => {
      if (key === 'conversation-search') {
        setSearchPanelMode('all');
        setSearchKeyword('');
        setIsSearchOpen(true);
        return;
      }

      if (key === 'conversation-refresh') {
        handleReloadPage();
        return;
      }

      if (key === 'conversation-apps') {
        openShareAppsPanel();
        return;
      }

      if (key === 'conversation-token-usage') {
        setIsTokenUsageOpen(true);
        return;
      }

      if (key === 'conversation-room-settings') {
        openSharedRoomSettings();
        return;
      }

      if (key === 'conversation-personal-settings') {
        openProgramTarget(buildShareManagementProgramTarget('app-settings', '개인설정'));
        return;
      }

      if (key === 'conversation-work-server-command') {
        openProgramTarget(buildShareManagementProgramTarget('server-command', '서버관리'));
        return;
      }

      if (key === 'conversation-clear') {
        void handleClearConversation();
      }
    },
    [handleClearConversation, handleReloadPage, openProgramTarget, openShareAppsPanel, openSharedRoomSettings],
  );

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const scrollContainer = scrollContainerRef.current;

    if (!scrollContainer || !isMobileTouchViewport()) {
      return undefined;
    }

    const isBlockedByOverlay =
      isSearchOpen
      || isTokenUsageOpen
      || isRoomSettingsOpen
      || isCreateRoomOpen
      || isOriginReplyModalOpen
      || Boolean(sourceGroupDetail)
      || Boolean(programTarget)
      || Boolean(activeProcessInspectorRequestId);

    let tracking: ShareEdgeGestureTracking | null = null;
    let lastTouch: Touch | null = null;

    const resetTracking = () => {
      tracking = null;
    };

    const handleTouchStart = (event: TouchEvent) => {
      const touch = event.touches[0];

      if (!touch || event.touches.length !== 1 || isBlockedByOverlay || isShareEdgeGestureIgnoredTarget(event.target)) {
        tracking = null;
        return;
      }

      if (
        touch.clientX >= window.innerWidth - SHARE_EDGE_APPS_GESTURE_HOTZONE_PX
      ) {
        const now = performance.now();
        event.preventDefault();
        tracking = {
          startX: touch.clientX,
          startY: touch.clientY,
          startTime: now,
          direction: 'apps',
          opened: false,
        };
        lastTouch = touch;
        return;
      }

      if (touch.clientX <= SHARE_EDGE_NAVIGATION_HOTZONE_PX) {
        const now = performance.now();
        tracking = {
          startX: touch.clientX,
          startY: touch.clientY,
          startTime: now,
          direction: 'back',
        };
        return;
      }

      tracking = null;
    };

    const handleTouchMove = (event: TouchEvent) => {
      const touch = event.touches[0];

      if (!tracking || !touch) {
        return;
      }

      lastTouch = touch;

      const deltaX = touch.clientX - tracking.startX;

      if (tracking.direction === 'apps') {
        if (deltaX <= SHARE_EDGE_GESTURE_OPEN_APPS_PX * -1) {
          if (deltaX <= SHARE_EDGE_GESTURE_MIN_HORIZONTAL_PX * -1) {
            event.preventDefault();
          }

          if (!tracking.opened) {
            tracking.opened = true;
            openShareAppsPanel();
          }
        }

        return;
      }

      if (deltaX <= SHARE_EDGE_GESTURE_MIN_HORIZONTAL_PX * -1) {
        return;
      }

      if (tracking.direction === 'back' && deltaX >= SHARE_EDGE_GESTURE_MIN_HORIZONTAL_PX) {
          event.preventDefault();
      }
    };

    const handleTouchEnd = (event: TouchEvent) => {
      const touch = event.changedTouches[0] ?? lastTouch;

      if (!tracking || !touch) {
        resetTracking();
        lastTouch = null;
        return;
      }

      if (tracking.direction === 'apps' && !tracking.opened) {
        const deltaX = touch.clientX - tracking.startX;

        if (deltaX <= SHARE_EDGE_GESTURE_OPEN_APPS_PX * -1) {
          tracking.opened = true;
          openShareAppsPanel();
        }
      }

      resetTracking();
      lastTouch = null;
    };

    const handleTouchCancel = () => {
      resetTracking();
      lastTouch = null;
    };

    scrollContainer.addEventListener('touchstart', handleTouchStart, { passive: false, capture: true });
    scrollContainer.addEventListener('touchmove', handleTouchMove, { passive: false, capture: true });
    scrollContainer.addEventListener('touchend', handleTouchEnd, { passive: true, capture: true });
    scrollContainer.addEventListener('touchcancel', handleTouchCancel, { passive: true, capture: true });

    return () => {
      scrollContainer.removeEventListener('touchstart', handleTouchStart, true);
      scrollContainer.removeEventListener('touchmove', handleTouchMove, true);
      scrollContainer.removeEventListener('touchend', handleTouchEnd, true);
      scrollContainer.removeEventListener('touchcancel', handleTouchCancel, true);
    };
  }, [
    activeProcessInspectorRequestId,
    isCreateRoomOpen,
    isOriginReplyModalOpen,
    isRoomSettingsOpen,
    isSearchOpen,
    isTokenUsageOpen,
    openShareAppsPanel,
    programTarget,
    sourceGroupDetail,
  ]);
  const shareComposerModelTokenAvailableLabel = useMemo(() => {
    return tokenUsageOverview.currentAvailableLabel;
  }, [tokenUsageOverview.currentAvailableLabel]);
  const handleSharedComposerModelChange = useCallback((nextCodexModel: string) => {
    setSharedComposerCodexModel(normalizeCodexModel(nextCodexModel));
  }, []);
  const shareComposerAttachmentItems = useMemo((): MenuProps['items'] => [
    {
      key: 'composer-attachment-files',
      label: (
        <span className="chat-share-page__settings-item">
          <span className="chat-share-page__settings-item-title">파일 첨부</span>
          <span className="chat-share-page__settings-item-description">컴퓨터에서 파일을 선택해 공유 채팅에 첨부합니다.</span>
        </span>
      ),
      icon: <FileTextOutlined />,
    },
    {
      key: 'composer-attachment-model',
      className: 'chat-share-page__composer-attachment-menu-model',
      label: (
        <div
          className="chat-share-page__composer-attachment-model-item"
          onPointerDown={(event) => event.stopPropagation()}
          onMouseDown={(event) => event.stopPropagation()}
          onClick={(event) => event.stopPropagation()}
        >
          <div className="chat-share-page__composer-attachment-model-title">모델 설정</div>
          <Select
            size="middle"
            value={sharedComposerCodexModel}
            aria-label="코덱스 모델"
            options={CODEX_MODEL_OPTIONS.map((option) => ({
              value: option.value,
              label: option.label,
            }))}
            onChange={handleSharedComposerModelChange}
            disabled={isSending}
            className="chat-share-page__composer-attachment-model-select"
            popupMatchSelectWidth={220}
          />
          <span className="chat-share-page__composer-attachment-model-token">{`잔여 ${shareComposerModelTokenAvailableLabel}`}</span>
          <span className="chat-share-page__composer-attachment-model-token">{`5시간 ${tokenUsageOverview.fiveHourUsagePercentLabel} 사용`}</span>
          <span className="chat-share-page__composer-attachment-model-token">{`1주일 ${tokenUsageOverview.sevenDayUsagePercentLabel} 사용`}</span>
          <span className="chat-share-page__composer-attachment-model-token chat-share-page__composer-attachment-model-token--session">{shareComposerModelTokenLabel}</span>
        </div>
      ),
    },
  ], [
    handleSharedComposerModelChange,
    isSending,
    sharedComposerCodexModel,
    shareComposerModelTokenAvailableLabel,
    shareComposerModelTokenLabel,
    tokenUsageOverview.fiveHourUsagePercentLabel,
    tokenUsageOverview.sevenDayUsagePercentLabel,
  ]);

  const shareExpandModeMenuItems = useMemo<MenuProps['items']>(
    () => [
      {
        key: 'latest',
        label: `마지막건${sortedRequests.length > 0 ? ` (${latestRequestIndex + 1}/${sortedRequests.length})` : ''}`,
      },
      {
        key: 'pending',
        label: `처리중·미확인 (${pendingCompletionRequests.length})`,
      },
      {
        key: 'all',
        label: `전체 (${sortedRequests.length})`,
      },
    ],
    [latestRequestIndex, pendingCompletionRequests.length, sortedRequests.length],
  );
  const handleSelectExpandMode: MenuProps['onClick'] = ({ key }) => {
    if (key === 'latest' || key === 'pending' || key === 'all') {
      if (key === 'latest') {
        const nextLatestRequest = sortedRequests[sortedRequests.length - 1];
        if (nextLatestRequest) {
          setLatestRequestId(nextLatestRequest.requestId);
        }
      }

      setExpandMode(key);
    }
  };

  useEffect(() => {
    const activePromptSelectionKeys = new Set<string>();

    sortedMessages.forEach((message) => {
      const promptParts = (messageRenderPayloadById.get(message.id)?.promptParts ?? []).map((part) =>
        normalizedToken ? rewritePromptPartForShare(part, normalizedToken) : part,
      );
      const parentRequestId = message.clientRequestId?.trim() || '';

      promptParts.forEach((target, promptIndex) => {
        activePromptSelectionKeys.add(
          buildSharePromptSelectionKey(
            parentRequestId,
            message.id,
            promptIndex,
            target.title,
            buildPromptTargetSignature(target),
          ),
        );
      });
    });

    if (promptTarget && promptTargetRequestId) {
      const rewrittenPromptTarget = normalizedToken ? rewritePromptPartForShare(promptTarget.prompt, normalizedToken) : promptTarget.prompt;
      activePromptSelectionKeys.add(
        buildSharePromptSelectionKey(
          promptTargetRequestId,
          promptTarget.sourceMessageId,
          promptTarget.promptIndex,
          rewrittenPromptTarget.title,
          buildPromptTargetSignature(rewrittenPromptTarget),
        ),
      );
    }

    setPendingPromptSelections((current) => {
      const nextEntries = Object.entries(current).filter(([key]) => activePromptSelectionKeys.has(key));

      if (nextEntries.length === Object.keys(current).length) {
        return current;
      }

      return Object.fromEntries(nextEntries);
    });
  }, [messageRenderPayloadById, normalizedToken, promptTarget, promptTargetRequestId, sortedMessages]);

  useEffect(() => {
    if (!hasActiveProcessingRequest) {
      return undefined;
    }

    setNowMs(Date.now());
    const intervalId = window.setInterval(() => {
      setNowMs(Date.now());
    }, SHARE_PROCESSING_CLOCK_INTERVAL_MS);

    return () => {
      window.clearInterval(intervalId);
    };
  }, [hasActiveProcessingRequest]);

  useEffect(() => {
    if (hasActiveProcessingRequest) {
      return undefined;
    }

    if (isTokenUsageOpen && selectedTokenUsageSetting) {
      setNowMs(Date.now());
      const intervalId = window.setInterval(() => {
        setNowMs(Date.now());
      }, SHARE_TOKEN_USAGE_CLOCK_INTERVAL_MS);

      return () => {
        window.clearInterval(intervalId);
      };
    }

    if (!currentShareEffectiveExpiresAt) {
      return undefined;
    }

    setNowMs(Date.now());
    const intervalId = window.setInterval(() => {
      setNowMs(Date.now());
    }, SHARE_EXPIRY_CLOCK_INTERVAL_MS);

    return () => {
      window.clearInterval(intervalId);
    };
  }, [currentShareEffectiveExpiresAt, hasActiveProcessingRequest, isTokenUsageOpen, selectedTokenUsageSetting]);

  useEffect(() => {
    return () => {
      if (liveRefreshTimerRef.current != null) {
        window.clearTimeout(liveRefreshTimerRef.current);
      }
    };
  }, []);

  useEffect(() => {
    queueScrollJumpVisibilitySync();

    return () => {
      if (scrollSyncFrameRef.current) {
        window.cancelAnimationFrame(scrollSyncFrameRef.current);
        scrollSyncFrameRef.current = null;
      }
      if (scrollIdleTimerRef.current) {
        window.clearTimeout(scrollIdleTimerRef.current);
        scrollIdleTimerRef.current = null;
      }
    };
  }, [displayedRequests, isPromptShare, queueScrollJumpVisibilitySync, sortedMessages.length]);

  if (isLoading) {
    return (
      <div className="chat-share-page chat-share-page--centered">
        <Spin size="large" />
      </div>
    );
  }

  if (requiresAccessPin) {
    return (
      <div className="chat-share-page chat-share-page--centered">
        <div className="chat-share-page__panel chat-share-page__empty-card chat-share-page__lock-card">
          <Title level={4}>공유 채팅방 비밀번호</Title>
          <Paragraph>이 공유 채팅방은 접근 전에 숫자 4자리 비밀번호 입력이 필요합니다.</Paragraph>
          <div className="chat-share-page__lock-form">
            <Input.Password
              value={accessPinInput}
              maxLength={SHARE_ACCESS_PIN_MAX_LENGTH}
              inputMode="numeric"
              autoComplete="one-time-code"
              placeholder="숫자 4자리"
              iconRender={renderAccessPinVisibilityIcon}
              onChange={(event) => {
                const nextValue = normalizeAccessPinInput(event.target.value);
                setAccessPinInput(nextValue);
                if (accessPinSubmitError) {
                  setAccessPinSubmitError('');
                }
                if (nextValue.length === SHARE_ACCESS_PIN_MAX_LENGTH) {
                  void handleUnlockShare(nextValue);
                }
              }}
              onPressEnter={() => {
                void handleUnlockShare();
              }}
            />
            {accessPinSubmitError ? <Text type="danger">{accessPinSubmitError}</Text> : null}
            <Button type="primary" onClick={() => void handleUnlockShare()}>
              잠금 해제
            </Button>
          </div>
        </div>
      </div>
    );
  }

  if (errorMessage || (!snapshot && hasLoadedShareData)) {
    return (
      <div className="chat-share-page chat-share-page--centered">
        <div className="chat-share-page__panel chat-share-page__empty-card">
          <Title level={4}>공유 화면을 열 수 없습니다.</Title>
          <Paragraph>{errorMessage || '공유 데이터가 없습니다.'}</Paragraph>
        </div>
      </div>
    );
  }

  if (!snapshot) {
    return (
      <div className="chat-share-page chat-share-page--centered">
        <Spin size="large" />
      </div>
    );
  }

  return (
    <>
      <div
        ref={scrollContainerRef}
        className={`chat-share-page${isComposerViewportCompacted ? ' chat-share-page--composer-viewport-compacted' : ''}`}
        onScroll={handleScrollContainerScroll}
        onFocusCapture={handleScrollContainerFocusCapture}
        onBlurCapture={handleScrollContainerBlurCapture}
      >
        <div ref={pageRef} className="chat-share-page__shell">
        {isPromptShare ? (
          <div className="chat-share-page__prompt-layout">
            <section className="chat-share-page__panel chat-share-page__panel--focus">
              <div className="chat-share-page__section-head">
                <div className="chat-share-page__section-copy">
                  <Title level={5}>공유된 prompt</Title>
                </div>
                {promptTarget &&
                promptTargetRequestId &&
                !isPromptResolved(promptTarget.prompt) &&
                !snapshot.targetRequest.manualPromptCompletedAt &&
                !isRequestInFlight(snapshot.targetRequest.status) ? (
                  <div className="chat-share-page__section-actions">
                    <Button
                      size="small"
                      icon={<CheckOutlined />}
                      loading={pendingPromptCompletionRequestIds.includes(promptTargetRequestId)}
                      onClick={() => {
                        void handleCompletePrompt(promptTargetRequestId);
                      }}
                    >
                      완료 처리
                    </Button>
                  </div>
                ) : null}
              </div>
              {promptTarget && promptTargetRequestId ? (
                (() => {
                  const target = rewritePromptPartForShare(promptTarget.prompt, normalizedToken);
                  const promptSignature = buildPromptTargetSignature(target);
                  const selectionKey = buildSharePromptSelectionKey(
                    promptTargetRequestId,
                    promptTarget.sourceMessageId,
                    promptTarget.promptIndex,
                    target.title,
                    promptSignature,
                  );
                  const draftSelection = pendingPromptSelections[selectionKey]?.status === 'draft'
                    ? pendingPromptSelections[selectionKey]
                    : null;
                  const submittedSelection = pendingPromptSelections[selectionKey]?.status === 'submitted'
                    ? pendingPromptSelections[selectionKey]
                    : null;

                  return (
                    <ChatPromptCard
                      target={target}
                      readOnly={Boolean(snapshot.targetRequest.manualPromptCompletedAt)}
                      draftSelection={draftSelection}
                      submittedSelection={submittedSelection}
                      onSelectionChange={(selection) => updatePendingPromptSelection(selectionKey, selection)}
                      onSubmitted={(selection) => markPendingPromptSelectionSubmitted(selectionKey, selection)}
                      onSubmit={(payload) =>
                        handleSubmitPrompt({
                          ...payload,
                          parentRequestId: promptTargetRequestId,
                          promptIndex: promptTarget.promptIndex,
                          sourceMessageId: promptTarget.sourceMessageId,
                        })
                      }
                      allowAttachments={Boolean(selectedShareRoomSessionId)}
                      attachmentAccept={SHARE_ATTACHMENT_ACCEPT}
                      onUploadAttachment={handleUploadPromptAttachment}
                    />
                  );
                })()
              ) : (
                <Text type="secondary">표시할 prompt가 없습니다.</Text>
              )}
            </section>

            {lastResponseMessage ? (
              <section className="chat-share-page__panel chat-share-page__panel--focus">
                <div className="chat-share-page__section-head">
                  <div className="chat-share-page__section-copy">
                    <Title level={5}>결과</Title>
                    <Text type="secondary">이 prompt에 대해 받은 최신 응답입니다.</Text>
                  </div>
                </div>
                <ShareResponseBlock
                  message={lastResponseMessage}
                  requestById={new Map(promptTargetRequestId ? [[promptTargetRequestId, snapshot.targetRequest]] : [])}
                  emphasisLabel="최신 결과"
                  onSubmitPrompt={handleSubmitPrompt}
                  promptSelections={pendingPromptSelections}
                  onPromptSelectionChange={updatePendingPromptSelection}
                  onPromptSubmitted={markPendingPromptSelectionSubmitted}
                  onCompletePrompt={handleCompletePrompt}
                  onCompleteVerification={handleCompleteVerification}
                  isPromptCompletionSaving={pendingPromptCompletionRequestIds.includes(promptTargetRequestId)}
                  isPromptManualCompleted={Boolean(snapshot.targetRequest.manualPromptCompletedAt)}
                  isVerificationCompletionSaving={pendingVerificationCompletionRequestIds.includes(promptTargetRequestId)}
                  isVerificationCompleted={Boolean(snapshot.targetRequest.manualVerificationCompletedAt)}
                  shareToken={normalizedToken}
                  canUploadAttachments={Boolean(selectedShareRoomSessionId)}
                  onUploadAttachment={handleUploadPromptAttachment}
                  onSetResponseAnchor={setResponseAnchorRef}
                  onCopyMessage={handleCopyShareMessageText}
                  onSetPromptAnchor={setPromptAnchorRef}
                />
              </section>
            ) : null}

            {promptShareFollowupRequests.length > 0 ? (
              <section className="chat-share-page__panel chat-share-page__panel--focus">
                <div className="chat-share-page__section-head">
                  <div className="chat-share-page__section-copy">
                    <Title level={5}>전달 내역</Title>
                    <Text type="secondary">선택 없이 보낸 기타 요청도 여기에서 계속 확인할 수 있습니다.</Text>
                  </div>
                </div>
                <div className="chat-share-page__message-list">
                  {promptShareFollowupRequests.map((request) => (
                    <ShareRequestCard
                      key={request.requestId}
                      request={request}
                      requestById={requestById}
                      answerText={requestAnswerTextById.get(request.requestId) ?? ''}
                      relatedMessages={requestMessagesById.get(request.requestId) ?? []}
                      mode="full"
                      onSubmitPrompt={handleSubmitPrompt}
                      promptSelections={pendingPromptSelections}
                      onPromptSelectionChange={updatePendingPromptSelection}
                      onPromptSubmitted={markPendingPromptSelectionSubmitted}
                      onCompletePrompt={handleCompletePrompt}
                      onCompleteVerification={handleCompleteVerification}
                      isPromptCompletionSaving={pendingPromptCompletionRequestIds.includes(request.requestId)}
                      isVerificationCompletionSaving={pendingVerificationCompletionRequestIds.includes(request.requestId)}
                      isVerificationCompleted={Boolean(request.manualVerificationCompletedAt)}
                      hasChildRequest={(childRequestCountByParentId.get(request.requestId.trim()) ?? 0) > 0}
                      activeReplyRequestId={null}
                      onCancelDisconnectedRequest={handleCancelDisconnectedRequest}
                      isRequestCancellationSaving={pendingRequestCancellationIds.includes(request.requestId)}
                      onRetryDisconnectedRequest={handleRetryDisconnectedRequest}
                      isRequestRetrySaving={pendingRequestRetryIds.includes(request.requestId)}
                      onReplyToResponse={null}
                      shareToken={normalizedToken}
                      onOpenProgram={openProgramTarget}
                      canUploadAttachments={Boolean(selectedShareRoomSessionId)}
                      onUploadAttachment={handleUploadPromptAttachment}
                      onSetRequestAnchor={setRequestAnchorRef}
                      onSetResponseAnchor={setResponseAnchorRef}
                      onSetPromptAnchor={setPromptAnchorRef}
                      onOpenProcessInspector={openProcessInspector}
                    />
                  ))}
                </div>
              </section>
            ) : null}
          </div>
        ) : (
          <div className={contentLayoutClassName}>
            <section className="chat-share-page__panel chat-share-page__conversation-panel">
              <div ref={conversationHeaderRef} className="chat-share-page__section-head">
                <div className="chat-share-page__section-copy">
                  <div className="chat-share-page__section-title-row">
                    <Title
                      level={5}
                      className="chat-share-page__conversation-title"
                      ellipsis={{ rows: 1, tooltip: headerTitleText.trim() || '채팅' }}
                    >
                      {headerTitleText.trim() || '채팅'}
                    </Title>
                    {canOpenSharedRoomSettings ? (
                      <Button
                        type="text"
                        size="small"
                        className="chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__section-action--inline chat-share-page__section-action--title-edit"
                        aria-label="채팅방 이름 및 설정 편집"
                        title="채팅방 이름 및 설정 편집"
                        icon={<EditOutlined />}
                        onClick={() => {
                          openSharedRoomSettings();
                        }}
                      />
                    ) : null}
                    <span
                      className={`chat-share-page__live-dot ${isLiveConnected ? 'chat-share-page__live-dot--connected' : 'chat-share-page__live-dot--disconnected'}`}
                      aria-label={isLiveConnected ? '웹소켓 연결됨' : '웹소켓 연결 끊김'}
                      title={isLiveConnected ? '웹소켓 연결됨' : '웹소켓 연결 끊김'}
                    />
                    {aggregateStatusTag ? (
                      <span
                        className={`chat-share-page__status-badge chat-share-page__status-badge--${aggregateStatusTag.color}`}
                        aria-label={aggregateStatusTag.label}
                        title={aggregateStatusTag.label}
                      >
                        <span className="chat-share-page__status-badge-label">{aggregateStatusTag.label}</span>
                      </span>
                    ) : null}
                  </div>
                  <Text type="secondary" className="chat-share-page__header-summary">
                    {headerSummaryLabel}
                  </Text>
                  {snapshot.hasOlderMessages ? (
                    <Alert
                      showIcon
                      type="info"
                      className="chat-share-page__initial-load-alert"
                      message="최근 대화만 먼저 불러왔습니다."
                      description="초기 진입 속도를 위해 최신 구간만 표시 중입니다. 필요할 때만 이전 대화를 페이지 단위로 더 불러오세요."
                      action={
                        <Button
                          size="small"
                          type="primary"
                          icon={<ThunderboltOutlined />}
                          loading={isLoadingFullSnapshot || isLoadingOlderShareHistory}
                          onClick={() => {
                            void handleLoadOlderShareHistory();
                          }}
                        >
                          이전 대화 더 불러오기
                        </Button>
                      }
                    />
                  ) : null}
                </div>
                <div className="chat-share-page__section-actions">
                  {canToggleShareRoomList ? (
                    <Button
                      ref={roomListTriggerButtonRef}
                      type="text"
                      size="small"
                      className="chat-share-page__section-action chat-share-page__section-action--icon"
                      icon={isShareRoomListVisible ? <EyeInvisibleOutlined /> : <AppstoreOutlined />}
                      aria-label={isShareRoomListVisible ? '채팅방 목록 숨기기' : '채팅방 목록 보기'}
                      title={isShareRoomListVisible ? '채팅방 목록 숨기기' : '채팅방 목록 보기'}
                      onClick={() => {
                        setIsShareRoomListVisible((current) => !current);
                      }}
                    />
                  ) : null}
                  <Dropdown
                    trigger={['click']}
                    menu={{
                      items: shareHeaderSettingsItems,
                      className: 'chat-share-page__settings-menu',
                      onClick: handleShareHeaderSettingsClick,
                    }}
                    placement="bottomRight"
                  >
                    <Button
                      type="text"
                      size="small"
                      className="chat-share-page__section-action chat-share-page__section-action--tool"
                      aria-label="채팅 설정"
                      title="채팅 설정"
                      icon={<SettingOutlined />}
                      >
                      <span className="chat-share-page__tool-button-label">설정</span>
                    </Button>
                  </Dropdown>
                </div>
              </div>
              <div
                ref={conversationToolbarRef}
                className="chat-share-page__conversation-toolbar"
                style={
                  {
                    '--chat-share-page-conversation-toolbar-top': `${conversationToolbarStickyTop}px`,
                  } as CSSProperties
                }
              >
                <div className="chat-share-page__conversation-toolbar-group" aria-label="요청 이동 및 필터">
                  <div className="chat-share-page__request-nav" aria-label="요청 이동">
                    <Button
                      type="text"
                      size="small"
                      className="chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__conversation-toolbar-button"
                      icon={<LeftOutlined />}
                      disabled={!canMoveToPreviousRequest}
                      onClick={handleMoveToPreviousRequest}
                    >
                      <span className="chat-share-page__tool-button-label">이전</span>
                    </Button>
                    <Button
                      type="text"
                      size="small"
                      className="chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__conversation-toolbar-button"
                      icon={<RightOutlined />}
                      iconPosition="end"
                      disabled={!canMoveToNextRequest}
                      onClick={handleMoveToNextRequest}
                    >
                      <span className="chat-share-page__tool-button-label">다음</span>
                    </Button>
                  </div>
                <Dropdown
                  trigger={['click']}
                  placement="bottomRight"
                  menu={{
                    items: shareExpandModeMenuItems,
                    selectable: true,
                    selectedKeys: [expandMode],
                    onClick: handleSelectExpandMode,
                  }}
                  >
                    <Button
                      type="text"
                      size="small"
                      className={`chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__conversation-toolbar-button chat-share-page__expand-mode-filter${expandMode !== 'latest' ? ' chat-share-page__expand-mode-filter--active' : ''}`}
                      aria-label={`공유 채팅 펼치기 필터: ${getShareExpandModeLabel(expandMode)} ${requestProgressLabel}`.trim()}
                      title={`공유 채팅 펼치기 필터: ${getShareExpandModeLabel(expandMode)} ${requestProgressLabel}`.trim()}
                      icon={<FilterOutlined />}
                  >
                    <span className="chat-share-page__tool-button-label">
                      {expandMode === 'latest' ? '필터' : getShareExpandModeLabel(expandMode)}
                    </span>
                    </Button>
                  </Dropdown>
                </div>
              </div>
              {showRoomSwitchingSkeleton ? (
                <div className="chat-share-page__conversation-loading-block" role="status" aria-live="polite">
                  <Spin size="large" />
                  <Text type="secondary">채팅방 내용을 불러오는 중입니다.</Text>
                </div>
              ) : (
                <div
                  className={`chat-share-page__message-list${showRoomSwitchingOverlay ? ' chat-share-page__message-list--switching' : ''}`}
                  aria-busy={showRoomSwitchingOverlay}
                >
                  {showRoomSwitchingOverlay ? (
                    <div className="chat-share-page__panel-switching-indicator" role="status" aria-live="polite">
                      <Spin size="small" />
                      <Text type="secondary">{`${roomSwitchingStatusLabel} 불러오는 중`}</Text>
                    </div>
                  ) : null}
                  {expandMode === 'latest' && hiddenBeforeCount > 0 ? (
                    <div className="chat-share-page__omission chat-share-page__omission--collapsed chat-share-page__omission--before" aria-label={`위쪽 채팅 ${hiddenBeforeCount}건 숨김`}>
                      <span className="chat-share-page__omission-line" aria-hidden="true" />
                      <Text type="secondary" className="chat-share-page__omission-label">{`위로 생략 ${hiddenBeforeCount}건`}</Text>
                      <span className="chat-share-page__omission-line" aria-hidden="true" />
                    </div>
                  ) : null}
                  {displayedRequests.map((request) => (
                    <ShareRequestCard
                      key={request.requestId}
                      request={request}
                      requestById={requestById}
                      answerText={requestAnswerTextById.get(request.requestId) ?? ''}
                      relatedMessages={requestMessagesById.get(request.requestId) ?? []}
                      mode="full"
                      onSubmitPrompt={handleSubmitPrompt}
                      promptSelections={pendingPromptSelections}
                      onPromptSelectionChange={updatePendingPromptSelection}
                      onPromptSubmitted={markPendingPromptSelectionSubmitted}
                      onCompletePrompt={handleCompletePrompt}
                      onCompleteVerification={handleCompleteVerification}
                      isPromptCompletionSaving={pendingPromptCompletionRequestIds.includes(request.requestId)}
                      isVerificationCompletionSaving={pendingVerificationCompletionRequestIds.includes(request.requestId)}
                      isVerificationCompleted={Boolean(request.manualVerificationCompletedAt)}
                      hasChildRequest={(childRequestCountByParentId.get(request.requestId.trim()) ?? 0) > 0}
                      activeReplyRequestId={replyReferenceRequestId}
                      onCancelDisconnectedRequest={handleCancelDisconnectedRequest}
                      isRequestCancellationSaving={pendingRequestCancellationIds.includes(request.requestId)}
                      onRetryDisconnectedRequest={handleRetryDisconnectedRequest}
                      isRequestRetrySaving={pendingRequestRetryIds.includes(request.requestId)}
                      onReplyToResponse={
                        isPromptShare
                          ? null
                          : (parentRequestId) => {
                              setReplyReferenceRequestId(parentRequestId.trim());
                              window.setTimeout(() => {
                                composerRef.current?.focus({ cursor: 'end' });
                              }, 0);
                            }
                      }
                      shareToken={normalizedToken}
                      onOpenProgram={openProgramTarget}
                      canUploadAttachments={Boolean(selectedShareRoomSessionId)}
                      onUploadAttachment={handleUploadPromptAttachment}
                      onSetRequestAnchor={setRequestAnchorRef}
                      onSetResponseAnchor={setResponseAnchorRef}
                      onSetPromptAnchor={setPromptAnchorRef}
                      onOpenPreviousQuestion={(requestId) => {
                        setPreviousQuestionModalRequestId(requestId.trim());
                      }}
                      onCopyMessage={handleCopyShareMessageText}
                      onCancelActiveRequest={handleCancelActiveShareRequest}
                      isActiveRequestCancellationSaving={pendingShareRuntimeRequestIds.includes(request.requestId)}
                      onResubmitRequestDirect={handleResubmitQueuedRequestDirect}
                      isDirectResubmitSaving={pendingShareRuntimeRequestIds.includes(request.requestId) || isSending}
                      onOpenProcessInspector={openProcessInspector}
                    />
                  ))}
                  {expandMode === 'latest' && hiddenAfterCount > 0 ? (
                    <div className="chat-share-page__omission chat-share-page__omission--collapsed chat-share-page__omission--after" aria-label={`아래쪽 채팅 ${hiddenAfterCount}건 숨김`}>
                      <span className="chat-share-page__omission-line" aria-hidden="true" />
                      <Text type="secondary" className="chat-share-page__omission-label">{`아래로 생략 ${hiddenAfterCount}건`}</Text>
                      <span className="chat-share-page__omission-line" aria-hidden="true" />
                    </div>
                  ) : null}
                  {expandMode === 'pending' && displayedRequests.length === 0 ? (
                    <div className="chat-share-page__omission chat-share-page__omission--empty">
                      <span className="chat-share-page__omission-line" aria-hidden="true" />
                      <Text type="secondary">현재 처리중이거나 확인이 필요한 건이 없습니다.</Text>
                      <span className="chat-share-page__omission-line" aria-hidden="true" />
                    </div>
                  ) : null}
                </div>
              )}
            </section>

            {canSendMessage ? (
              <>
                {expandMode === 'latest' && collapsedActivitySummary.length > 0 ? (
                  <section
                    id="chat-share-activity-panel"
                    className={`chat-share-page__panel chat-share-page__activity-panel${showRoomSwitchingOverlay ? ' chat-share-page__panel--switching' : ''}`}
                    aria-label="현재 진행 상황"
                    aria-busy={showRoomSwitchingOverlay}
                  >
                    {showRoomSwitchingOverlay ? (
                      <div className="chat-share-page__panel-switching-indicator chat-share-page__panel-switching-indicator--compact" role="status" aria-live="polite">
                        <Spin size="small" />
                        <Text type="secondary">새 방 상태 반영 중</Text>
                      </div>
                    ) : null}
                    <div className="chat-share-page__section-head chat-share-page__section-head--compact">
                      <div className="chat-share-page__section-copy">
                        <Title level={5}>현재 진행 상황</Title>
                      </div>
                    </div>
                    <div className="chat-share-page__activity-summary-list">
                      {collapsedActivitySummary.map((item) => (
                        <Text key={item} type="secondary" className="chat-share-page__activity-summary-item">{item}</Text>
                      ))}
                    </div>
                  </section>
                ) : null}
                <section className="chat-share-page__panel chat-share-page__composer-panel">
                  {showRoomSwitchingSkeleton ? (
                    <div className="chat-share-page__composer-loading-block" role="status" aria-live="polite">
                      <Spin size="large" />
                    </div>
                  ) : (
                    <div
                      className={`chat-share-page__composer-shell app-chat-panel__composer${showRoomSwitchingOverlay ? ' chat-share-page__composer-shell--switching' : ''}`}
                      aria-busy={showRoomSwitchingOverlay}
                    >
                    {showRoomSwitchingOverlay ? (
                      <div className="chat-share-page__panel-switching-indicator chat-share-page__panel-switching-indicator--composer" role="status" aria-live="polite">
                        <Spin size="small" />
                        <Text type="secondary">전환이 끝나면 입력할 수 있습니다.</Text>
                      </div>
                    ) : null}
                    <input
                      ref={composerAttachmentInputRef}
                      type="file"
                      multiple
                      accept={SHARE_ATTACHMENT_ACCEPT}
                      className="chat-share-page__composer-file-input"
                      onChange={(event) => {
                        const files = Array.from(event.target.files ?? []);
                        event.target.value = '';
                        void handleUploadComposerAttachments(files);
                      }}
                    />
                    <div className="chat-share-page__composer-topline">
                      <div className="app-chat-panel__composer-utility-buttons">
                        <Dropdown
                          trigger={['click']}
                          menu={{
                            items: shareComposerAttachmentItems,
                            onClick: handleShareComposerAttachmentMenuClick,
                          }}
                          overlayClassName="chat-share-page__composer-attachment-menu"
                          disabled={isSending || isUploadingComposerAttachment}
                          className="chat-share-page__composer-attachment-dropdown"
                          placement="top"
                        >
                          <Button
                            type="text"
                            icon={<PlusOutlined />}
                            aria-label="첨부 옵션"
                            title="첨부 옵션"
                            loading={isUploadingComposerAttachment}
                            className="chat-share-page__composer-utility-button"
                            onPointerDown={(event) => {
                              event.preventDefault();
                            }}
                          />
                        </Dropdown>
                      </div>
                      <div className="app-chat-panel__composer-type chat-share-page__composer-type-readonly">
                        <Select
                          value={shareChatTypeLabel}
                          aria-label="현재 채팅유형"
                          options={[
                            {
                              value: shareChatTypeLabel,
                              label: (
                                <div className="app-chat-panel__type-option">
                                  <span>{shareChatTypeLabel}</span>
                                </div>
                              ),
                            },
                          ]}
                          disabled
                        />
                      </div>
                      <div className="app-chat-panel__composer-actions chat-share-page__composer-topline-actions">
                        <div className="app-chat-panel__composer-action-buttons">
                          <Button
                            icon={<ThunderboltOutlined />}
                            type={isImmediateSendPinned ? 'primary' : 'default'}
                            title={isImmediateSendPinned ? '즉시전송 항상 켜짐' : '즉시 전송'}
                            aria-label={isImmediateSendPinned ? '즉시전송 항상 켜짐' : '즉시 전송'}
                            onPointerDown={startImmediateSendHoldTimer}
                            onPointerUp={clearImmediateSendHoldTimer}
                            onPointerLeave={clearImmediateSendHoldTimer}
                            onPointerCancel={clearImmediateSendHoldTimer}
                            onClick={handleImmediateSendButtonClick}
                            loading={isSending}
                            disabled={(!draftText.trim() && composerAttachments.length === 0) || isUploadingComposerAttachment}
                          />
                          <Button
                            type="primary"
                            icon={<SendOutlined />}
                            title={isImmediateSendPinned ? '즉시전송으로 보내기' : '대기열로 보내기'}
                            aria-label={isImmediateSendPinned ? '즉시전송으로 보내기' : '대기열로 보내기'}
                            onClick={() => void handleSendMessage()}
                            loading={isSending}
                            disabled={(!draftText.trim() && composerAttachments.length === 0) || isUploadingComposerAttachment}
                          />
                        </div>
                      </div>
                    </div>
                    {replyReferenceRequest ? (
                      <div className="chat-share-page__reply-reference" aria-live="polite">
                        <div className="chat-share-page__reply-reference-copy">
                          <span className="chat-share-page__reply-reference-label">답변 참조 중</span>
                          <span className="chat-share-page__reply-reference-text">{replyReferenceSummary}</span>
                        </div>
                        <Button
                          type="text"
                          size="small"
                          className="chat-share-page__reply-reference-clear"
                          onClick={() => {
                            setReplyReferenceRequestId('');
                          }}
                        >
                          해제
                        </Button>
                      </div>
                    ) : null}
                    <div className="chat-share-page__composer-entry-row">
                      <div
                        ref={composerInputShellRef}
                        className={`app-chat-panel__composer-input-shell chat-share-page__composer-input-shell${
                          isSending ? ' chat-share-page__composer-input-shell--sending' : ''
                        }`}
                      >
                        <Input.TextArea
                          ref={composerRef}
                          value={draftText}
                          onChange={(event) => setDraftText(event.target.value)}
                          onKeyDown={handleComposerKeyDown}
                          onPaste={handleComposerPaste}
                          onFocus={handleComposerFocus}
                          onBlur={handleComposerBlur}
                          placeholder={
                            replyReferenceRequest
                              ? '선택한 답변을 바탕으로 공유채팅에 이어서 보낼 내용을 입력하세요. 첨부만 추가해서 보내도 됩니다.'
                              : '공유채팅에 보낼 내용을 입력하세요. 첨부만 추가해서 보내도 됩니다.'
                          }
                          aria-busy={isSending}
                          rows={1}
                          maxLength={20000}
                          disabled={isSending || isUploadingComposerAttachment}
                        />
                      </div>
                    </div>
                    {composerAttachments.length > 0 ? (
                      <div className="app-chat-panel__composer-attachment-strip" aria-live="polite">
                        {composerAttachments.map((attachment) => (
                          <div key={attachment.id} className="app-chat-panel__composer-attachment-chip">
                            <span className="app-chat-panel__composer-attachment-name">{attachment.name}</span>
                            <Button
                              type="text"
                              size="small"
                              className="app-chat-panel__composer-attachment-remove"
                              icon={<ShareCloseIcon />}
                              aria-label={`${attachment.name} 첨부 제거`}
                              onClick={() => {
                                setComposerAttachments((current) => current.filter((item) => item.id !== attachment.id));
                              }}
                            />
                          </div>
                        ))}
                      </div>
                    ) : null}
                    </div>
                  )}
                </section>
              </>
            ) : !isPromptShare && shareBlockedReason ? (
              <section className="chat-share-page__panel chat-share-page__composer-panel">
                <Alert
                  showIcon
                  type="warning"
                  message="이 공유 링크에서는 더 이상 답변을 보낼 수 없습니다."
                  description={shareBlockedReason}
                />
              </section>
            ) : null}
          </div>
        )}
        </div>
      </div>
      {showScrollToTop || showScrollToBottom ? (
        <div className="chat-share-page__scroll-jump">
          {showScrollToTop ? (
            <Button type="primary" shape="circle" icon={<UpOutlined />} aria-label="채팅 처음으로 이동" onClick={handleScrollToTop} />
          ) : null}
          {showScrollToBottom ? (
            <Button type="primary" shape="circle" icon={<DownOutlined />} aria-label="채팅 끝으로 이동" onClick={handleScrollToBottom} />
          ) : null}
        </div>
      ) : null}
      <Modal
        open={Boolean(pendingDeleteRoom)}
        title="공유 채팅방을 삭제할까요?"
        okText="삭제"
        cancelText="취소"
        okButtonProps={{ danger: true }}
        centered
        onCancel={() => {
          setPendingDeleteRoomSessionId('');
        }}
        onOk={async () => {
          if (!pendingDeleteRoom) {
            return;
          }

          await handleDeleteShareRoom(pendingDeleteRoom);
        }}
      >
        <Text>
          {pendingDeleteRoom
            ? `"${pendingDeleteRoom.title}" 채팅방과 이 방의 요청·메시지 기록이 삭제됩니다.`
            : '선택한 공유 채팅방과 이 방의 요청·메시지 기록이 삭제됩니다.'}
        </Text>
      </Modal>
      <Modal
        open={Boolean(previousQuestionModalDirectParent || previousQuestionModalTopParent)}
        title="부모 질의"
        footer={null}
        className="chat-share-page__previous-question-modal-dialog"
        onCancel={() => {
          setPreviousQuestionModalRequestId('');
        }}
      >
        <div className="chat-share-page__previous-question-modal">
          {previousQuestionModalDirectParent ? (
            <div className="chat-share-page__previous-question-modal-section">
              <div className="chat-share-page__previous-question-modal-head">
                <Text strong>바로 상위 부모</Text>
                <Text type="secondary">
                  {formatTimeLabel(previousQuestionModalDirectParent.createdAt) || '요청 시각 없음'}
                </Text>
              </div>
              <Paragraph className="chat-share-page__previous-question-modal-text">
                {previousQuestionModalDirectParentText || '부모 질의 내용을 찾지 못했습니다.'}
              </Paragraph>
              {previousQuestionModalDirectParentPreviewItems.length > 0 ? (
                <div className="chat-share-page__resource-list">
                  {previousQuestionModalDirectParentPreviewItems.map((item) => (
                    <ShareResourcePreviewCard
                      key={`previous-question-direct-${item.id}`}
                      item={item}
                      shareToken={normalizedToken}
                      onOpenProgram={openProgramTarget}
                    />
                  ))}
                </div>
              ) : null}
            </div>
          ) : null}
          {previousQuestionModalTopParent ? (
            <div className="chat-share-page__previous-question-modal-section">
              <div className="chat-share-page__previous-question-modal-head">
                <Text strong>연결된 최상위 부모</Text>
                <Text type="secondary">
                  {formatTimeLabel(previousQuestionModalTopParent.createdAt) || '요청 시각 없음'}
                </Text>
              </div>
              <Paragraph className="chat-share-page__previous-question-modal-text">
                {previousQuestionModalTopParentText || '최상위 부모 질의 내용을 찾지 못했습니다.'}
              </Paragraph>
              {previousQuestionModalTopParentPreviewItems.length > 0 ? (
                <div className="chat-share-page__resource-list">
                  {previousQuestionModalTopParentPreviewItems.map((item) => (
                    <ShareResourcePreviewCard
                      key={`previous-question-top-${item.id}`}
                      item={item}
                      shareToken={normalizedToken}
                      onOpenProgram={openProgramTarget}
                    />
                  ))}
                </div>
              ) : null}
            </div>
          ) : null}
          {!previousQuestionModalDirectParent && !previousQuestionModalTopParent ? (
            <div className="chat-share-page__previous-question-modal-section">
              <Paragraph className="chat-share-page__previous-question-modal-text">
                부모 질의 내용을 찾지 못했습니다.
              </Paragraph>
            </div>
          ) : null}
        </div>
      </Modal>
      <Modal
        open={isTokenUsageOpen}
        footer={null}
        title="토큰 관리"
        className="chat-share-page__token-usage-modal"
        onCancel={() => setIsTokenUsageOpen(false)}
      >
        <div className="chat-share-page__token-usage-modal-body">
          {selectedTokenUsageSetting ? (
            <div className="chat-share-page__token-usage-select-row">
              <Text type="secondary">적용 토큰 설정</Text>
              <Text strong>{`${selectedTokenUsageSetting.name} (${selectedTokenUsageSetting.id})`}</Text>
            </div>
          ) : null}
          <div className="chat-share-page__token-usage-overview-card" aria-label="토큰 집계 요약">
            <div className="chat-share-page__token-usage-overview-head">
              <div>
                <div className="chat-share-page__token-usage-overview-label">지금 사용 가능</div>
                <div className="chat-share-page__token-usage-overview-value">{tokenUsageOverview.currentAvailableLabel}</div>
              </div>
              <Tag color={tokenUsageFiveHourSummary.requestCount > 0 ? 'gold' : 'default'}>
                {tokenUsageOverview.fiveHourBucketLabel}
              </Tag>
            </div>
            <div className="chat-share-page__token-usage-meter-card">
              <div className="chat-share-page__token-usage-meter-track chat-share-page__token-usage-meter-track--merged" aria-hidden="true">
                {tokenUsageOverview.meterLayers.map((item) => (
                  <span
                    key={item.key}
                    className={`chat-share-page__token-usage-meter-fill ${item.colorClassName}`}
                    style={{ width: `${item.widthPercent}%` }}
                  />
                ))}
              </div>
              <div className="chat-share-page__token-usage-meter-legend">
                {tokenUsageOverview.meterLegendItems.map((item) => (
                  <div key={item.key} className="chat-share-page__token-usage-meter-row">
                    <span className={`chat-share-page__token-usage-meter-dot ${item.colorClassName}`} aria-hidden="true" />
                    <span className="chat-share-page__token-usage-meter-label">{item.label}</span>
                    <span className="chat-share-page__token-usage-meter-value">{item.caption}</span>
                  </div>
                ))}
              </div>
            </div>
            <div className="chat-share-page__token-usage-summary-copy">{tokenUsageOverview.oneLineSummary}</div>
            <div className="chat-share-page__token-usage-reset-grid">
              <div className="chat-share-page__token-usage-reset-card chat-share-page__token-usage-reset-card--primary">
                <span className="chat-share-page__token-usage-reset-label">5시간 초기화까지</span>
                <strong className="chat-share-page__token-usage-reset-value">{tokenUsageOverview.fiveHourCountdownLabel}</strong>
                <span className="chat-share-page__token-usage-summary-copy">
                  {tokenUsageOverview.fiveHourResetTimeLabel} · {tokenUsageOverview.fiveHourResetAmountLabel}
                </span>
              </div>
              <div className="chat-share-page__token-usage-reset-card">
                <span className="chat-share-page__token-usage-reset-label">1주일 초기화</span>
                <strong className="chat-share-page__token-usage-reset-value chat-share-page__token-usage-reset-value--compact">
                  {tokenUsageOverview.sevenDayResetTimeLabel}
                </strong>
              </div>
            </div>
          </div>
          <div className="chat-share-page__token-usage-select-row">
            <Text type="secondary">현재 공유 토큰</Text>
            <div className="chat-share-page__token-usage-share-url-row">
              <Paragraph
                className="chat-share-page__token-usage-share-url"
                ellipsis={currentShareUrl ? { rows: 1, tooltip: currentShareUrl } : { rows: 1 }}
                style={{ maxWidth: '100%', marginBottom: 0 }}
              >
                {currentShareUrl || 'URL 없음'}
              </Paragraph>
              {currentShareUrl ? (
                <Button
                  type="text"
                  size="small"
                  className="chat-share-page__token-usage-copy-button"
                  icon={<CopyOutlined />}
                  aria-label="토큰 URL 복사"
                  onClick={() => {
                    void handleCopyCurrentShareUrl();
                  }}
                />
              ) : null}
            </div>
            <div className="chat-share-page__token-usage-token-meta">
              <Text type="secondary">{currentShareTokenUsageStatusLabel}</Text>
            </div>
          </div>
          <div className="chat-share-page__token-usage-summary-copy">
            {selectedTokenUsageSetting
              ? `현재 토큰 ${
                  currentShareEffectiveExpiresAt ? currentShareExpiresAtLabel : currentShareDefaultPolicyLabel
                } · 기본 정책 ${
                  currentShareDefaultExpiryLabel ? currentShareDefaultExpiryLabel.replace(/^기본 유효시간\s*/, '') : '사용기간 제한 없음'
                }`
              : '토큰 설정이 없으면 사용량만 집계합니다.'}
          </div>
        </div>
      </Modal>
      <Drawer
        open={isRoomSettingsOpen}
        title="공유 채팅방 설정"
        placement="right"
        width="100vw"
        className="chat-share-page__room-settings-drawer"
        onClose={() => {
          if (isSavingRoomSettings) {
            return;
          }

          setIsRoomSettingsOpen(false);
        }}
        extra={(
          <div className="chat-share-page__room-settings-actions">
            <Button
              onClick={() => setIsRoomSettingsOpen(false)}
              disabled={isSavingRoomSettings}
            >
              취소
            </Button>
            <Button
              type="primary"
              loading={isSavingRoomSettings}
              disabled={
                isSavingRoomSettings ||
                (!canManageSharedRoomSettings && !canEditSharedRoomAccessPin) ||
                (canManageSharedRoomSettings && (isChatTypesLoading || isChatContextSettingsLoading))
              }
              onClick={() => {
                void handleSaveSharedRoomSettings();
              }}
            >
              {canManageSharedRoomSettings || canEditSharedRoomAccessPin ? '저장' : '닫기'}
            </Button>
          </div>
        )}
      >
        <div className="chat-share-page__room-settings-shell">
          {chatTypesErrorMessage ? <Alert showIcon type="error" message={chatTypesErrorMessage} /> : null}
          {chatContextSettingsErrorMessage ? <Alert showIcon type="error" message={chatContextSettingsErrorMessage} /> : null}
          {!canManageSharedRoomSettings ? (
            <Alert
              showIcon
              type="info"
              message={
                canEditSharedRoomAccessPin
                  ? '이 공유 링크는 채팅유형과 문맥은 읽기 전용이며, 비밀번호만 변경할 수 있습니다.'
                  : '이 공유 링크는 채팅방 설정을 읽기 전용으로만 볼 수 있습니다.'
              }
            />
          ) : null}
          <Tabs
            activeKey={roomSettingsTabKey}
            onChange={(value) => setRoomSettingsTabKey(value as typeof roomSettingsTabKey)}
            className="chat-share-page__room-settings-tabs"
            items={[
              {
                key: 'chat-type',
                label: '채팅유형',
                children: (
                  <div className="chat-share-page__room-settings-panel">
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>채팅방 이름</Text>
                      <Text type="secondary">공유 채팅 헤더와 알림에 표시할 이름을 직접 저장합니다.</Text>
                    </div>
                    <Input
                      value={editingRoomTitle}
                      placeholder="예: 관리자 공유 채팅"
                      readOnly={!canManageSharedRoomSettings}
                      maxLength={200}
                      onChange={(event) => {
                        setEditingRoomTitle(event.target.value);
                      }}
                    />
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>기본 채팅유형</Text>
                      <Text type="secondary">공유채팅이 기본으로 사용할 유형을 먼저 고릅니다.</Text>
                    </div>
                    <Select
                      value={editingRoomChatTypeId ?? undefined}
                      placeholder="채팅유형 선택"
                      loading={isChatTypesLoading}
                      disabled={!canManageSharedRoomSettings}
                      onChange={(value) => {
                        setEditingRoomChatTypeId(value);
                        if (!isEditingRoomDefaultContextsDirty) {
                          setEditingRoomDefaultContextIds(resolveShareRoomDefaultContextIds(activeRoomContextSettings, chatTypeDefaults, value));
                        }
                      }}
                      options={enabledChatTypes.map((item) => ({
                        value: item.id,
                        label: item.name,
                      }))}
                    />
                    <div className="chat-share-page__room-settings-card">
                      <Text strong>{enabledChatTypes.find((item) => item.id === editingRoomChatTypeId)?.name ?? '채팅유형 선택 필요'}</Text>
                      <Text type="secondary">
                        {enabledChatTypes.find((item) => item.id === editingRoomChatTypeId)?.description || '선택한 채팅유형의 기본 설명이 여기에 표시됩니다.'}
                      </Text>
                    </div>
                  </div>
                ),
              },
              {
                key: 'default-contexts',
                label: '공통 문맥',
                children: (
                  <div className="chat-share-page__room-settings-panel">
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>적용 공통 문맥</Text>
                      <Text type="secondary">비워 두면 선택한 채팅유형의 기본 공통 문맥을 그대로 상속합니다.</Text>
                    </div>
                    {enabledDefaultContexts.length > 0 ? (
                      <Checkbox.Group
                        className="chat-share-page__room-settings-checkbox-group"
                        value={editingRoomDefaultContextIds}
                        disabled={!canManageSharedRoomSettings || isChatContextSettingsLoading}
                        onChange={(checkedValues) => {
                          setIsEditingRoomDefaultContextsDirty(true);
                          setEditingRoomDefaultContextIds(
                            checkedValues
                              .map((value) => String(value).trim())
                              .filter((value) => enabledDefaultContexts.some((context) => context.id === value)),
                          );
                        }}
                      >
                        {enabledDefaultContexts.map((item) => (
                          <label key={item.id} className="chat-share-page__room-settings-card">
                            <Checkbox value={item.id}>{item.title}</Checkbox>
                            <Text type="secondary">
                              {item.content.split('\n')[0]?.replace(/^#+\s*/, '') || '설명 없음'}
                            </Text>
                          </label>
                        ))}
                      </Checkbox.Group>
                    ) : (
                      <Alert showIcon type="info" message="등록된 공통 문맥이 없습니다." />
                    )}
                  </div>
                ),
              },
              {
                key: 'room-context',
                label: '방 전용',
                children: (
                  <div className="chat-share-page__room-settings-panel">
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>방 전용 문맥</Text>
                      <Text type="secondary">이 공유채팅방에서만 추가로 참조할 규칙을 적습니다.</Text>
                    </div>
                    <Input
                      value={editingRoomCustomContextTitle}
                      placeholder="예: 공유 검수용 응답 규칙"
                      readOnly={!canManageSharedRoomSettings}
                      onChange={(event) => {
                        setEditingRoomCustomContextTitle(event.target.value);
                      }}
                    />
                    <Input.TextArea
                      value={editingRoomCustomContextContent}
                      rows={14}
                      placeholder="이 공유 채팅방에만 적용할 추가 규칙을 입력하세요."
                      readOnly={!canManageSharedRoomSettings}
                      onChange={(event) => {
                        setEditingRoomCustomContextContent(event.target.value);
                      }}
                    />
                  </div>
                ),
              },
              {
                key: 'notifications',
                label: '채팅 알림',
                children: (
                  <div className="chat-share-page__room-settings-panel">
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>이 채팅방 알림</Text>
                      <Text type="secondary">현재 기기에서 이 공유채팅방 새 답변 알림을 받을지 설정합니다.</Text>
                    </div>
                    <label className="chat-share-page__room-settings-toggle-card">
                      <Checkbox
                        checked={editingRoomNotifyOffline}
                        disabled={!canManageSharedRoomSettings}
                        onChange={(event) => {
                          setEditingRoomNotifyOffline(event.target.checked);
                        }}
                      >
                        채팅방 알림 수신
                      </Checkbox>
                      <Text type="secondary">
                        {editingRoomNotifyOffline
                          ? '새 답변이 도착하면 이 브라우저 클라이언트 기준으로 알림 대상에 포함합니다.'
                          : '이 기기는 현재 공유채팅방 오프라인 알림 대상에서 제외됩니다.'}
                      </Text>
                    </label>
                    <div className="chat-share-page__room-settings-card chat-share-page__room-settings-card--status">
                      <Text strong>{roomNotificationClientStatus.summaryLabel}</Text>
                      <div className="chat-share-page__room-settings-status-tags">
                        <Tag color={editingRoomNotifyOffline ? 'green' : 'default'}>{roomNotificationClientStatus.roomLabel}</Tag>
                        <Tag color={appConfig.chat.receiveRoomNotifications ? 'green' : 'default'}>{roomNotificationClientStatus.appLabel}</Tag>
                        <Tag color={roomNotificationClientStatus.tone === 'warning' ? 'gold' : 'default'}>
                          {roomNotificationClientStatus.permissionLabel}
                        </Tag>
                        <Tag color={roomNotificationClientStatus.tone === 'success' ? 'green' : 'default'}>
                          {roomNotificationClientStatus.registrationLabel}
                        </Tag>
                      </div>
                      <Text type="secondary">
                        방 설정, 앱 전체 허용, 브라우저 권한, 현재 기기 등록 상태만 간단히 합쳐서 표시합니다.
                      </Text>
                    </div>
                    <div className="chat-share-page__room-settings-inline-actions">
                      <Button size="small" onClick={() => void refreshRoomNotificationStatus(editingRoomNotifyOffline)} loading={isRefreshingRoomNotificationStatus}>
                        상태 다시 확인
                      </Button>
                      <Button
                        size="small"
                        type="primary"
                        icon={<SendOutlined />}
                        onClick={() => void handleSendRoomNotificationTest()}
                        loading={isSendingRoomNotificationTest}
                      >
                        테스트 전송
                      </Button>
                    </div>
                  </div>
                ),
              },
              {
                key: 'security',
                label: '보안',
                children: (
                  <div className="chat-share-page__room-settings-panel">
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>공유 비밀번호</Text>
                      <Text type="secondary">공유 URL 자체에 4자리 비밀번호를 걸어 접근을 제한합니다.</Text>
                    </div>
                    <label className="chat-share-page__room-settings-toggle-card">
                      <Checkbox
                        checked={editingRoomUseAccessPin}
                        disabled={!canEditSharedRoomAccessPin}
                        onChange={(event) => {
                          setEditingRoomUseAccessPin(event.target.checked);
                          if (!event.target.checked) {
                            setEditingRoomAccessPin('');
                          }
                        }}
                      >
                        공유 URL 접근 시 숫자 4자리 비밀번호 요구
                      </Checkbox>
                    </label>
                    <Input.Password
                      value={editingRoomAccessPin}
                      maxLength={4}
                      inputMode="numeric"
                      autoComplete="one-time-code"
                      iconRender={renderAccessPinVisibilityIcon}
                      placeholder={
                        editingRoomUseAccessPin
                          ? snapshot?.share.hasAccessPin
                            ? '변경할 때만 새 4자리를 입력'
                            : '숫자 4자리'
                          : '사용 안 함'
                      }
                      disabled={!editingRoomUseAccessPin || !canEditSharedRoomAccessPin}
                      onChange={(event) => {
                        setEditingRoomAccessPin(event.target.value);
                      }}
                    />
                    <Select
                      value={resolveAccessPinPromptTtlOptionValue(editingRoomAccessPinPromptTtlMinutes)}
                      disabled={!editingRoomUseAccessPin || !canEditSharedRoomAccessPin}
                      options={SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS.map((item) => ({
                        value: item.value,
                        label: item.label,
                      }))}
                      onChange={(value) => {
                        setEditingRoomAccessPinPromptTtlMinutes(parseAccessPinPromptTtlOptionValue(value));
                      }}
                    />
                    <Text type="secondary">
                      {!canManageSharedRoomSettings && !canEditSharedRoomAccessPin
                        ? '현재 비밀번호 상태만 확인할 수 있습니다.'
                        : editingRoomUseAccessPin
                          ? snapshot?.share.hasAccessPin
                            ? (editingRoomAccessPinPromptTtlMinutes ?? 0) > 0
                              ? `현재 비밀번호가 설정되어 있습니다. 유지하려면 비워 두고 저장하세요. 한 번 잠금 해제하면 ${resolveAccessPinPromptTtlLabel(editingRoomAccessPinPromptTtlMinutes ?? 0)} 동안은 새로고침해도 다시 묻지 않습니다.`
                              : '현재 비밀번호가 설정되어 있습니다. 유지하려면 비워 두고 저장하세요. 공유채팅방에 다시 접근할 때마다 4자리를 다시 입력합니다.'
                            : (editingRoomAccessPinPromptTtlMinutes ?? 0) > 0
                              ? `비밀번호를 켜면 공유받은 사용자는 진입 전에 4자리를 입력해야 합니다. 한 번 잠금 해제하면 ${resolveAccessPinPromptTtlLabel(editingRoomAccessPinPromptTtlMinutes ?? 0)} 동안은 새로고침해도 다시 묻지 않습니다.`
                              : '비밀번호를 켜면 공유받은 사용자는 공유채팅방에 접근할 때마다 4자리를 입력해야 합니다.'
                        : '비밀번호를 끄면 기존 잠금이 있더라도 해제됩니다. 이 비밀번호는 공유받은 사용자가 필요할 때 직접 설정하는 용도입니다.'}
                    </Text>
                  </div>
                ),
              },
              {
                key: 'runtime',
                label: `처리중 세션${shareRuntimeRunningItems.length + shareRuntimeQueuedItems.length > 0 ? ` (${shareRuntimeRunningItems.length + shareRuntimeQueuedItems.length})` : ''}`,
                children: (
                  <div className="chat-share-page__room-settings-panel">
                    <div className="chat-share-page__room-settings-panel-head">
                      <Text strong>현재 방 실행 상태</Text>
                      <Text type="secondary">이 공유채팅방의 실행중·대기 요청을 보고, 상세 과정 창에서 처리 시간·계획·실행 설명까지 확인할 수 있습니다.</Text>
                    </div>
                    <div className="chat-share-page__room-settings-card chat-share-page__room-settings-card--status">
                      <div>
                        <Text strong>{shareRuntimeRunningItems.length}건</Text>
                        <Text type="secondary">실행중</Text>
                      </div>
                      <div>
                        <Text strong>{shareRuntimeQueuedItems.length}건</Text>
                        <Text type="secondary">대기중</Text>
                      </div>
                      <div>
                        <Text strong>{shareRuntimeRecentItems.length}건</Text>
                        <Text type="secondary">최근 종료</Text>
                      </div>
                      <div>
                        <Text strong>{formatShareRuntimeTimestamp(shareRuntimeSnapshot?.generatedAt)}</Text>
                        <Text type="secondary">마지막 확인</Text>
                      </div>
                    </div>
                    <div className="chat-share-page__room-settings-inline-actions">
                      <Button
                        size="small"
                        icon={<ReloadOutlined />}
                        loading={isShareRuntimeLoading}
                        onClick={() => {
                          void refreshShareRuntime();
                        }}
                      >
                        상태 새로고침
                      </Button>
                    </div>
                    {shareRuntimeRunningItems.length === 0 && shareRuntimeQueuedItems.length === 0 ? (
                      <Alert showIcon type="info" message="현재 이 방에서 처리 중이거나 대기 중인 요청이 없습니다." />
                    ) : null}
                    {shareRuntimeRunningItems.length > 0 ? (
                      <div className="chat-share-page__room-settings-panel">
                        <div className="chat-share-page__room-settings-panel-head">
                          <Text strong>실행중</Text>
                        </div>
                        <div className="chat-share-page__room-settings-runtime-list">
                          {shareRuntimeRunningItems.map((item) => {
                            const statusTag = resolveShareRuntimeStatusTag(item);
                            const isPending = pendingShareRuntimeRequestIds.includes(item.requestId);

                            return (
                              <div key={item.requestId} className="chat-share-page__room-settings-runtime-card">
                                <div className="chat-share-page__room-settings-runtime-head">
                                  <Tag color={statusTag.color}>{statusTag.label}</Tag>
                                  <Text type="secondary">{formatElapsedDuration(item.startedAt ?? item.enqueuedAt, nowMs) || '-'}</Text>
                                </div>
                                <Text strong>{item.summary || '요약 정보 없음'}</Text>
                                <Text type="secondary">{`요청 ${item.requestId}`}</Text>
                                <Text type="secondary">{`시작 ${formatShareRuntimeTimestamp(item.startedAt ?? item.enqueuedAt)}`}</Text>
                                <div className="chat-share-page__room-settings-inline-actions">
                                  <Button
                                    size="small"
                                    icon={<EyeOutlined />}
                                    onClick={() => {
                                      openProcessInspector(item.requestId);
                                    }}
                                  >
                                    상세 과정
                                  </Button>
                                  <Button
                                    size="small"
                                    danger
                                    loading={isPending}
                                    disabled={!canManageSharedRoomSettings}
                                    onClick={() => handleCancelShareRuntimeRequest(item)}
                                  >
                                    취소
                                  </Button>
                                </div>
                              </div>
                            );
                          })}
                        </div>
                      </div>
                    ) : null}
                    {shareRuntimeQueuedItems.length > 0 ? (
                      <div className="chat-share-page__room-settings-panel">
                        <div className="chat-share-page__room-settings-panel-head">
                          <Text strong>대기중</Text>
                        </div>
                        <div className="chat-share-page__room-settings-runtime-list">
                          {shareRuntimeQueuedItems.map((item) => {
                            const statusTag = resolveShareRuntimeStatusTag(item);
                            const isPending = pendingShareRuntimeRequestIds.includes(item.requestId);

                            return (
                              <div key={item.requestId} className="chat-share-page__room-settings-runtime-card">
                                <div className="chat-share-page__room-settings-runtime-head">
                                  <Tag color={statusTag.color}>{statusTag.label}</Tag>
                                  <Text type="secondary">{formatElapsedDuration(item.enqueuedAt, nowMs) || '-'}</Text>
                                </div>
                                <Text strong>{item.summary || '요약 정보 없음'}</Text>
                                <Text type="secondary">{`요청 ${item.requestId}`}</Text>
                                <Text type="secondary">{`대기 시작 ${formatShareRuntimeTimestamp(item.enqueuedAt)}`}</Text>
                                <div className="chat-share-page__room-settings-inline-actions">
                                  <Button
                                    size="small"
                                    icon={<EyeOutlined />}
                                    onClick={() => {
                                      openProcessInspector(item.requestId);
                                    }}
                                  >
                                    상세 과정
                                  </Button>
                                  <Button
                                    size="small"
                                    danger
                                    loading={isPending}
                                    disabled={!canManageSharedRoomSettings}
                                    onClick={() => handleCancelShareRuntimeRequest(item)}
                                  >
                                    취소
                                  </Button>
                                </div>
                              </div>
                            );
                          })}
                        </div>
                      </div>
                    ) : null}
                    {shareRuntimeRecentItems.length > 0 ? (
                      <div className="chat-share-page__room-settings-panel">
                        <div className="chat-share-page__room-settings-panel-head">
                          <Text strong>최근 종료</Text>
                        </div>
                        <div className="chat-share-page__room-settings-runtime-list">
                          {shareRuntimeRecentItems.map((item) => {
                            const terminalTag = resolveShareRuntimeTerminalTag(item.terminalStatus);

                            return (
                              <div key={`${item.requestId}-${item.lastUpdatedAt}`} className="chat-share-page__room-settings-runtime-card">
                                <div className="chat-share-page__room-settings-runtime-head">
                                  <Tag color={terminalTag.color}>{terminalTag.label}</Tag>
                                  <Text type="secondary">{formatShareRuntimeTimestamp(item.lastUpdatedAt)}</Text>
                                </div>
                                <Text strong>{item.summary || '요약 정보 없음'}</Text>
                                <Text type="secondary">{`요청 ${item.requestId}`}</Text>
                              </div>
                            );
                          })}
                        </div>
                      </div>
                    ) : null}
                    {!canManageSharedRoomSettings ? (
                      <Alert showIcon type="info" message="현재 공유 링크는 실행 상태 조회만 가능하고 취소는 관리 권한이 있을 때만 허용됩니다." />
                    ) : null}
                  </div>
                ),
              },
            ]}
          />
        </div>
      </Drawer>
      <Modal
        open={isCreateRoomOpen}
        title="공유 채팅방 추가"
        okText="추가"
        cancelText="취소"
        confirmLoading={isCreatingRoom}
        onOk={() => {
          void handleCreateShareRoom();
        }}
        onCancel={() => {
          if (isCreatingRoom) {
            return;
          }
          setIsCreateRoomOpen(false);
        }}
      >
        <div className="chat-share-page__create-room-form">
          <label className="chat-share-page__create-room-field">
            <span>추천 세션 연결</span>
            <Select
              allowClear
              showSearch
              loading={isLoadingConversationCandidates}
              value={creatingRoomLinkedSessionId || undefined}
              placeholder="기존 세션을 고르면 연결된 작업방으로 만듭니다."
              optionFilterProp="label"
              options={conversationCandidates.map((item) => ({
                value: item.sessionId,
                label: `${item.title || item.contextLabel || item.sessionId} · ${item.lastRequestPreview || item.lastMessagePreview || '미리보기 없음'}`,
              }))}
              onChange={(value) => {
                setCreatingRoomLinkedSessionId(value ?? '');
              }}
            />
            {creatingRoomLinkedConversation ? (
              <Text type="secondary" className="chat-share-page__create-room-hint">
                {creatingRoomLinkedConversation.lastRequestPreview || creatingRoomLinkedConversation.lastMessagePreview || '원 세션 미리보기 없음'}
              </Text>
            ) : (
              <Text type="secondary" className="chat-share-page__create-room-hint">
                선택하지 않으면 독립 공유채팅방으로 생성합니다.
              </Text>
            )}
          </label>
          <label className="chat-share-page__create-room-field">
            <span>채팅방 이름</span>
            <Input
              value={creatingRoomTitle}
              maxLength={200}
              placeholder="예: 문구 검토 2번방"
              onChange={(event) => setCreatingRoomTitle(event.target.value)}
            />
          </label>
          <label className="chat-share-page__create-room-field">
            <span>채팅유형</span>
            <Select
              value={creatingRoomChatTypeId ?? undefined}
              options={enabledChatTypes.map((item) => ({
                value: item.id,
                label: item.name,
              }))}
              onChange={(value) => setCreatingRoomChatTypeId(value)}
            />
          </label>
          <label className="chat-share-page__create-room-field">
            <span>요청 뱃지</span>
            <Input
              value={creatingRoomRequestBadgeLabel}
              maxLength={120}
              placeholder="선택 입력"
              onChange={(event) => setCreatingRoomRequestBadgeLabel(event.target.value)}
            />
          </label>
          <label className="chat-share-page__create-room-field">
            <span>시작 문구</span>
            <Input.TextArea
              rows={5}
              value={creatingRoomSeedMessage}
              placeholder="이 방에 처음 남길 질문이나 안내를 입력하세요."
              onChange={(event) => setCreatingRoomSeedMessage(event.target.value)}
            />
          </label>
        </div>
      </Modal>
      <Modal
        open={Boolean(sourceGroupDetail)}
        title="연결 세션 상세"
        footer={null}
        onCancel={() => setSourceGroupDetailKey('')}
      >
        {sourceGroupDetail ? (
          <div className="chat-share-page__create-room-form">
            <div className="chat-share-page__source-detail-card">
              <Text strong>{sourceGroupDetail.title}</Text>
              <Text type="secondary">{sourceGroupDetail.chatTypeLabel || '연결된 원 세션'}</Text>
              <Paragraph className="chat-share-page__source-detail-preview">
                {sourceGroupDetail.requestPreview || '등록된 원 요청 미리보기가 없습니다.'}
              </Paragraph>
              <div className="chat-share-page__room-group-actions">
                <Button size="small" onClick={() => handleMoveToSourceSession(sourceGroupDetail)}>원 세션 이동</Button>
                {sourceGroupDetail.rooms.some((room) => room.sessionId === selectedShareRoomSessionId) && lastResponseMessage?.text?.trim() ? (
                  <Button
                    size="small"
                    type="primary"
                    onClick={() => {
                      setSourceGroupDetailKey('');
                      setOriginReplyTargetGroupKey(sourceGroupDetail.key);
                      setOriginReplyDraftText(buildShareVisibleText(lastResponseMessage.text));
                      setIsOriginReplyModalOpen(true);
                    }}
                  >
                    답변하기
                  </Button>
                ) : null}
              </div>
            </div>
            <div className="chat-share-page__source-room-chip-list">
              {sourceGroupDetail.rooms.map((room) => (
                <Button
                  key={room.sessionId}
                  size="small"
                  className="chat-share-page__source-room-chip"
                  type={room.sessionId === selectedShareRoomSessionId ? 'primary' : 'default'}
                  onClick={() => {
                    setSourceGroupDetailKey('');
                    handleSelectShareRoom(room.sessionId);
                  }}
                >
                  {room.title}
                </Button>
              ))}
            </div>
          </div>
        ) : null}
      </Modal>
      <Modal
        open={isOriginReplyModalOpen}
        title="원 세션으로 답변 전송"
        okText="전송"
        cancelText="취소"
        confirmLoading={isSubmittingOriginReply}
        onOk={() => {
          void handleSubmitOriginReply();
        }}
        onCancel={() => {
          if (isSubmittingOriginReply) {
            return;
          }
          setIsOriginReplyModalOpen(false);
          setOriginReplyTargetGroupKey('');
        }}
      >
        <div className="chat-share-page__create-room-form">
          <Text type="secondary">
            {originReplyTargetGroup?.title || '선택된 원 세션'} 기준으로 현재 작업 답변을 전달합니다.
          </Text>
          <Input.TextArea
            rows={7}
            value={originReplyDraftText}
            placeholder="원 세션에 보낼 답변을 입력하세요."
            onChange={(event) => setOriginReplyDraftText(event.target.value)}
          />
        </div>
      </Modal>
      <Modal
        open={isSearchOpen}
        footer={null}
        title={searchPanelMode === 'apps' ? '공유채팅방 Apps' : '공유채팅방 통합검색'}
        className="chat-share-page__search-modal"
        onCancel={() => setIsSearchOpen(false)}
      >
        <div className="chat-share-page__search-modal-body">
          <Input
            autoFocus
            allowClear
            size="large"
            prefix={<SearchOutlined />}
            placeholder={searchPanelMode === 'apps' ? '허용된 Apps 검색' : '질문, 답변, 리소스, 활동 로그 검색'}
            value={searchKeyword}
            onChange={(event) => setSearchKeyword(event.target.value)}
          />
          <div className="chat-share-page__search-summary">
            <Text type="secondary">
              {searchKeyword.trim()
                ? `검색 결과 ${searchResults.length}건`
                : searchPanelMode === 'apps'
                  ? '최근 자주 연 앱을 먼저 보여주며, 아이콘 중심으로 빠르게 실행할 수 있습니다.'
                  : '질문, 답변, 리소스, 활동 로그를 함께 찾습니다.'}
            </Text>
          </div>
          {searchPanelMode === 'apps' ? (
            <div className="chat-share-page__search-app-environment">
              <Text type="secondary">실행 환경</Text>
              <Select
                size="middle"
                value={selectedAppEnvironment}
                options={SHARE_APP_ENVIRONMENT_OPTIONS.map((environment) => ({
                  label: environment.label,
                  value: environment.key,
                }))}
                onChange={(value) => setSelectedAppEnvironment(value)}
              />
            </div>
          ) : null}
          <div className={`chat-share-page__search-results${searchPanelMode === 'apps' ? ' chat-share-page__search-results--apps' : ''}`}>
            {searchResults.length > 0 ? (
              searchResults.map((result) => (
                searchPanelMode === 'apps' ? (
                  <button
                    key={result.key}
                    type="button"
                    className={`chat-share-page__app-tile${
                      result.appEntry && !isPlayAppSupportedInEnvironment(result.appEntry, selectedAppEnvironment)
                        ? ' chat-share-page__app-tile--disabled'
                        : ''
                    }`}
                    disabled={Boolean(result.appEntry) && !isPlayAppSupportedInEnvironment(result.appEntry, selectedAppEnvironment)}
                    onClick={() => handleSearchResultSelect(result)}
                  >
                    <span className="chat-share-page__app-tile-icon">{result.icon ?? <AppstoreOutlined />}</span>
                    <span className="chat-share-page__app-tile-title">{result.title}</span>
                    <span className="chat-share-page__app-tile-description">
                      {result.appEntry ? `지원 ${result.description}` : result.description}
                    </span>
                    <span className="chat-share-page__app-tile-meta">
                      <span className="chat-share-page__app-tile-meta-label">
                        {result.appEntry
                          ? isPlayAppSupportedInEnvironment(result.appEntry, selectedAppEnvironment)
                            ? `${selectedAppEnvironment} 실행`
                            : '환경 미지원'
                          : result.resource?.appId === SHARE_CURRENT_CHAT_APP_ID
                            ? `${selectedAppEnvironment} 열기`
                          : '관리 앱'}
                      </span>
                      {result.usageBadge ? (
                        <span className="chat-share-page__app-tile-usage">{result.usageBadge}</span>
                      ) : null}
                    </span>
                  </button>
                ) : (
                  <div key={result.key} className="chat-share-page__search-result">
                    <button type="button" className="chat-share-page__search-result-main" onClick={() => handleSearchResultSelect(result)}>
                      <span className="chat-share-page__search-result-title">{result.title}</span>
                      <span className="chat-share-page__search-result-description">{result.description}</span>
                    </button>
                    {result.appEntry ? (
                      <div className="chat-share-page__search-result-action-group">
                        <Tag bordered={false} className="chat-share-page__search-result-tag">
                          {`지원 ${resolveSupportedEnvironmentSummary(result.appEntry)}`}
                        </Tag>
                        <Button
                          type="text"
                          size="small"
                          disabled={!isPlayAppSupportedInEnvironment(result.appEntry, selectedAppEnvironment)}
                          className="chat-share-page__search-result-action chat-share-page__search-result-action--environment"
                          onClick={() => openAllowedPlayAppEnvironment(result.appEntry, selectedAppEnvironment)}
                        >
                          {isPlayAppSupportedInEnvironment(result.appEntry, selectedAppEnvironment) ? '앱 실행' : '미지원'}
                        </Button>
                      </div>
                    ) : result.resource ? (
                      <Button
                        type="text"
                        size="small"
                        className="chat-share-page__search-result-action"
                        icon={<AppstoreOutlined />}
                        onClick={() => { if (result.resource) openProgramTarget(result.resource); }}
                      >
                        실행
                      </Button>
                    ) : null}
                  </div>
                )
              ))
            ) : (
              <div className="chat-share-page__search-empty">
                <Text type="secondary">
                  {searchKeyword.trim() ? '검색 결과가 없습니다.' : searchPanelMode === 'apps' ? '허용된 Apps가 없습니다.' : '검색어를 입력해 주세요.'}
                </Text>
              </div>
            )}
          </div>
        </div>
      </Modal>
      <Drawer
        open={isServerCommandDrawerOpen}
        title={programTarget?.label ?? '서버관리'}
        rootClassName="chat-share-page__server-command-drawer-shell"
        className="chat-share-page__server-command-drawer"
        placement={isServerCommandDrawerMobile ? 'bottom' : 'right'}
        width={isServerCommandDrawerMobile ? undefined : '100vw'}
        height={isServerCommandDrawerMobile ? '100dvh' : undefined}
        maskClosable={false}
        destroyOnHidden={false}
        extra={(
          <Button
            type="text"
            className="fullscreen-preview-modal__icon-button"
            aria-label="서버관리 새로고침"
            title="서버관리 새로고침"
            icon={<ReloadOutlined />}
            onClick={handleReloadProgram}
          />
        )}
        onClose={closeProgramTarget}
      >
        {isServerCommandDrawerOpen ? (
          <div
            key={`${programTarget.key}:${programReloadKey}`}
            className="chat-share-page__server-command-drawer-body"
          >
            <ServerCommandPage sharedAccess={sharedServerCommandAccess} />
          </div>
        ) : null}
      </Drawer>
      <Drawer
        open={isAppSettingsDrawerOpen}
        title="개인설정"
        rootClassName="chat-share-page__app-settings-drawer-shell"
        className="chat-share-page__app-settings-drawer"
        placement={isServerCommandDrawerMobile ? 'bottom' : 'right'}
        width={isServerCommandDrawerMobile ? undefined : '100vw'}
        height={isServerCommandDrawerMobile ? '100dvh' : undefined}
        maskClosable={false}
        destroyOnHidden={false}
        extra={(
          <Button
            type="text"
            className="fullscreen-preview-modal__icon-button"
            aria-label="개인설정 새로고침"
            title="개인설정 새로고침"
            icon={<ReloadOutlined />}
            onClick={handleReloadProgram}
          />
        )}
        onClose={closeProgramTarget}
      >
        {isAppSettingsDrawerOpen ? (
          <div className="chat-share-page__app-settings-drawer-body">
            <SharedAppSettingsPage shareToken={normalizedToken} />
          </div>
        ) : null}
      </Drawer>
      <FullscreenPreviewModal
        open={Boolean(programTarget) && !isServerCommandDrawerOpen && !isAppSettingsDrawerOpen}
        title={programTarget?.label ?? '공유 프로그램'}
        meta={programTarget?.meta ?? '공유 토큰 실행'}
        actions={
          programTarget ? (
            <Button
              type="text"
              className="fullscreen-preview-modal__icon-button"
              aria-label="앱 새로고침"
              title="앱 새로고침"
              icon={<ReloadOutlined />}
              onClick={handleReloadProgram}
            />
          ) : null
        }
        zIndex={SHARE_PROGRAM_MODAL_Z_INDEX}
        maskClosable={false}
        hideHeader={false}
        onMinimize={programTarget ? handleMinimizeProgram : null}
        onClose={closeProgramTarget}
        className={`chat-share-page__program-modal${
          ''
        }`}
        contentClassName="chat-share-page__program-modal-content"
        fillContent
      >
        {programTarget ? (
          embeddedPlayAppContent ? (
            <div key={`${programTarget.key}:${programReloadKey}`} className="chat-share-page__program-app-shell">
              {embeddedPlayAppContent}
            </div>
          ) : programTarget.appId && findReadyPlayAppEntryById(programTarget.appId) ? (
            <iframe
              key={`${programTarget.key}:${programReloadKey}`}
              title={programTarget.label}
              src={programTarget.url}
              className="app-chat-panel__preview-frame"
            />
          ) : programTarget.appId === SHARE_CURRENT_CHAT_APP_ID ? (
            <iframe
              key={`${programTarget.key}:${programReloadKey}`}
              title={programTarget.label}
              src={programTarget.url}
              className="app-chat-panel__preview-frame"
            />
          ) : programTarget.appId === 'text-memo-widget' ? (
            <div
              key={`${programTarget.key}:${programReloadKey}`}
              className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
            >
              <Suspense
                fallback={(
                  <div className="chat-share-page__program-app-loading" role="status" aria-live="polite">
                    <Spin size="large" />
                  </div>
                )}
              >
                <LazyTextMemoWidget cardWrapper={false} headerless skin="flat" title="공유 메모" />
              </Suspense>
            </div>
          ) : programTarget.appId === 'token-setting' ? (
            <div
              key={`${programTarget.key}:${programReloadKey}`}
              className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
            >
              <TokenSettingManagementPage
                sharedPreviewTokenSetting={shareTokenSetting}
                sharedAccess={
                  canManageSharedTokenSetting
                    ? {
                        shareToken: normalizedToken,
                        canManage: true,
                      }
                    : null
                }
              />
            </div>
          ) : programTarget.appId === 'shared-resource' ? (
            <div
              key={`${programTarget.key}:${programReloadKey}`}
              className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
            >
              <SharedResourceManagementPage
                disableInstallMetadata
                sharedPreview={{
                  managedResourceTokenId: snapshot?.share.managedResourceTokenId ?? null,
                  sharePath: snapshot?.share.sharePath ?? null,
                  expiresAt: snapshot?.share.expiresAt ?? null,
                  tokenSetting: shareTokenSetting,
                }}
                sharedAccess={
                  sharePermissionSet.has('manage') && shareAllowedAppIdSet.has('shared-resource')
                    ? {
                        shareToken: normalizedToken,
                        managedResourceTokenId: snapshot?.share.managedResourceTokenId ?? null,
                      }
                    : null
                }
              />
            </div>
          ) : (
            <div key={`${programTarget.key}:${programReloadKey}`} className="chat-share-page__program-app-shell">
              <ChatPreviewBody
                target={{
                  label: programTarget.label,
                  url: programTarget.url,
                  kind: programTarget.kind,
                }}
                previewText=""
                isPreviewLoading={false}
                previewError=""
                renderHtmlAsFrame
                fullscreen
              />
            </div>
          )
          ) : null}
      </FullscreenPreviewModal>
      {typeof document !== 'undefined' && minimizedProgramCards.length > 0
        ? createPortal(<>{minimizedProgramCards}</>, document.body)
        : minimizedProgramCards}
      {typeof document !== 'undefined' && processInspectorCard
        ? createPortal(processInspectorCard, document.body)
        : processInspectorCard}
      {typeof document !== 'undefined' && canToggleShareRoomList && isShareRoomListVisible && shareRoomListLayerStyle
        ? createPortal(
          <section
            ref={roomListPanelRef}
            className="chat-share-page__panel chat-share-page__room-list-panel chat-share-page__room-list-panel--floating"
            style={shareRoomListLayerStyle}
          >
            <Input
              allowClear
              value={shareRoomFilterKeyword}
              onChange={(event) => {
                setShareRoomFilterKeyword(event.target.value);
              }}
              className="chat-share-page__room-filter-input"
              placeholder="채팅방 필터"
              prefix={<SearchOutlined />}
              aria-label="공유채팅 채팅방 필터"
            />
            <div className="chat-share-page__section-head chat-share-page__section-head--compact">
              <div className="chat-share-page__section-copy">
                <Title level={5}>채팅방</Title>
                <Text type="secondary">
                  {activeShareRoom ? `${activeShareRoom.title} 사용 중` : '공유 토큰에 연결된 방 목록'}
                </Text>
              </div>
              {canCreateSharedRooms ? (
                <Button size="small" icon={<PlusOutlined />} onClick={openCreateRoomDialog}>
                  추가
                </Button>
              ) : null}
            </div>
            <div className="chat-share-page__room-list">
              {filteredShareRoomGroups.map((group) => (
                <div
                  key={group.key}
                  className={`chat-share-page__room-group${group.linkContext ? '' : ' chat-share-page__room-group--standalone'}`}
                >
                  {group.linkContext ? (
                    <div className="chat-share-page__room-group-head">
                      <div className="chat-share-page__room-group-copy">
                        <span className="chat-share-page__room-group-title">{group.title}</span>
                        <span className="chat-share-page__room-group-meta">
                          {group.chatTypeLabel || '연결된 원 세션'}
                        </span>
                        {group.requestPreview ? (
                          <span className="chat-share-page__room-group-preview">{group.requestPreview}</span>
                        ) : null}
                      </div>
                      <div className="chat-share-page__room-group-actions">
                        <Button
                          type="text"
                          size="small"
                          className="chat-share-page__room-group-action"
                          onClick={() => setSourceGroupDetailKey(group.key)}
                        >
                          상세
                        </Button>
                        <Button
                          type="text"
                          size="small"
                          className="chat-share-page__room-group-action"
                          onClick={() => handleMoveToSourceSession(group)}
                        >
                          이동
                        </Button>
                        {group.rooms.some((room) => room.sessionId === selectedShareRoomSessionId) && lastResponseMessage?.text?.trim() ? (
                          <Button
                            type="text"
                            size="small"
                            className="chat-share-page__room-group-action"
                            onClick={() => {
                              setOriginReplyTargetGroupKey(group.key);
                              setOriginReplyDraftText(buildShareVisibleText(lastResponseMessage.text));
                              setIsOriginReplyModalOpen(true);
                            }}
                          >
                            답변하기
                          </Button>
                        ) : null}
                      </div>
                    </div>
                  ) : null}
                  <div className="chat-share-page__room-group-list">
                    {group.rooms.map((room) => {
                      const isActive = room.sessionId === selectedShareRoomSessionId;
                      const canDeleteRoom = canDeleteShareRoom(room, shareRooms);
                      const isDeletingTarget = isDeletingRoom && pendingDeleteRoomSessionId === room.sessionId;
                      const pendingCounts = shareRoomPendingCountsBySessionId[room.sessionId] ?? null;

                      return (
                        <div
                          key={room.sessionId}
                          className={`chat-share-page__room-item${canDeleteRoom ? '' : ' is-delete-locked'}`}
                        >
                          {canDeleteRoom ? (
                            <button
                              type="button"
                              className="chat-share-page__room-delete-action"
                              aria-label={`${room.title} 채팅방 삭제`}
                              title={`${room.title} 채팅방 삭제`}
                              disabled={isDeletingTarget}
                              onClick={() => {
                                setIsShareRoomListVisible(true);
                                setPendingDeleteRoomSessionId(room.sessionId);
                              }}
                            >
                              <DeleteOutlined />
                            </button>
                          ) : null}
                          <button
                            type="button"
                            className={`chat-share-page__room-card${isActive ? ' chat-share-page__room-card--active' : ''}${
                              room.isDefault ? ' chat-share-page__room-card--default' : ''
                            }`}
                            onClick={() => {
                              handleSelectShareRoom(room.sessionId);
                            }}
                          >
                            <span className="chat-share-page__room-card-head">
                              <span className="chat-share-page__room-card-title">{room.title}</span>
                              {room.isDefault ? <Tag color="cyan">기본 채팅</Tag> : null}
                            </span>
                            <span className="chat-share-page__room-card-meta">
                              {room.isDefault
                                ? (room.contextLabel?.trim() || room.requestBadgeLabel?.trim() || '삭제할 수 없는 기본 채팅방')
                                : (room.contextLabel?.trim() || room.requestBadgeLabel?.trim() || '공유 채팅방')}
                            </span>
                            <span className="chat-share-page__room-card-counts" aria-label="채팅방 처리 상태">
                              {pendingCounts ? (
                                <>
                                  <span className="chat-share-page__room-card-count chat-share-page__room-card-count--processing">
                                    처리중 {pendingCounts.processingCount}
                                  </span>
                                  <span className="chat-share-page__room-card-count chat-share-page__room-card-count--unanswered">
                                    미확인 {pendingCounts.unansweredCount}
                                  </span>
                                </>
                              ) : (
                                <span className="chat-share-page__room-card-count chat-share-page__room-card-count--idle">
                                  {isLoadingShareRoomPendingCounts ? '건수 확인 중' : '건수 준비 중'}
                                </span>
                              )}
                            </span>
                            {!group.linkContext && group.requestPreview ? (
                              <span className="chat-share-page__room-card-preview">{group.requestPreview}</span>
                            ) : null}
                          </button>
                        </div>
                      );
                    })}
                  </div>
                </div>
              ))}
              {filteredShareRoomGroups.length === 0 ? (
                <div className="chat-share-page__room-list-empty">
                  <Text type="secondary">조건에 맞는 채팅방이 없습니다.</Text>
                </div>
              ) : null}
            </div>
          </section>,
          document.body,
        )
        : null}
    </>
  );
}
